Merge "Expose instanceName in the subject template"
diff --git a/.mailmap b/.mailmap
index f5f8f3e..4c71059 100644
--- a/.mailmap
+++ b/.mailmap
@@ -10,6 +10,7 @@
Bruce Zu <bruce.zu.run10@gmail.com> <bruce.zu@sonyericsson.com>
Bruce Zu <bruce.zu.run10@gmail.com> <bruce.zu@sonymobile.com>
Carlos Eduardo Baldacin <carloseduardo.baldacin@sonyericsson.com> carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
+Changcheng Xiao <xchangcheng@google.com> xchangcheng
Dariusz Luksza <dluksza@collab.net> <dariusz@luksza.org>
Dave Borowitz <dborowitz@google.com> <dborowitz@google.com>
David Ostrovsky <david@ostrovsky.org> <d.ostrovsky@gmx.de>
@@ -67,6 +68,7 @@
Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjolin <ulrik.sjolin@gmail.com>
Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjölin <ulrik.sjolin@sonyericsson.com>
Ulrik Sjölin <ulrik.sjolin@sonyericsson.com> Ulrik Sjolin <ulrik.sjolin@sonyericsson.com>
+Viktar Donich <viktard@google.com> viktard
Yuxuan 'fishy' Wang <fishywang@google.com> Yuxuan Wang <fishywang@google.com>
Zalán Blénessy <zalanb@axis.com> Zalan Blenessy <zalanb@axis.com>
飞 李 <lifei@7v1.net> lifei <lifei@7v1.net>
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 72e309a..397b99a 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1380,6 +1380,13 @@
link:cmd-stream-events.html[stream Gerrit events via ssh].
+[[capability_viewAccess]]
+=== View Access
+
+Allow checking access rights for arbitrary (user, project) pairs,
+using the link:rest-api-projects.html#check-access[check.access]
+endpoint
+
[[capability_viewAllAccounts]]
=== View All Accounts
diff --git a/Documentation/cmd-index-activate.txt b/Documentation/cmd-index-activate.txt
index 418e872..4428d12 100644
--- a/Documentation/cmd-index-activate.txt
+++ b/Documentation/cmd-index-activate.txt
@@ -31,12 +31,13 @@
Currently supported values:
* changes
* accounts
+ * groups
== EXAMPLES
Activate the latest change index:
----
- $ ssh -p 29418 review.example.com gerrit activate changes
+ $ ssh -p 29418 review.example.com gerrit index activate changes
----
GERRIT
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index eed2eb4..f535281 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -169,6 +169,9 @@
link:cmd-plugin-remove.html[gerrit plugin rm]::
Alias for 'gerrit plugin remove'.
+link:cmd-reload-config.html[gerrit reload-config]::
+ Apply an updated gerrit.config.
+
link:cmd-set-account.html[gerrit set-account]::
Change an account's settings.
diff --git a/Documentation/cmd-reload-config.txt b/Documentation/cmd-reload-config.txt
new file mode 100644
index 0000000..7a25130
--- /dev/null
+++ b/Documentation/cmd-reload-config.txt
@@ -0,0 +1,44 @@
+= plugin reload
+
+== NAME
+reload-config - Reloads the gerrit.config.
+
+== SYNOPSIS
+[verse]
+--
+_ssh_ -p <port> <host> _gerrit reload-config_
+ <NAME> ...
+--
+
+== DESCRIPTION
+Reloads the gerrit.config configuration.
+
+Not all configuration values can be picked up by this command. Which config
+sections and values that are supported is documented here:
+link:config-gerrit.html[Configuration]
+
+_The output shows only modified config values that are picked up by Gerrit
+and applied._
+
+If a config entry is added or removed from gerrit.config, but still brings
+no effect due to a matching default value, no output for this entry is shown.
+
+== ACCESS
+* Caller must be a member of the privileged 'Administrators' group.
+
+== SCRIPTING
+This command is intended to be used in scripts.
+
+== EXAMPLES
+Reload the gerrit configuration:
+
+----
+ ssh -p 29418 localhost gerrit reload-config
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index a75f610..72a9c21 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -264,7 +264,7 @@
=== Work In Progress State Changed
-Sent when the the link:intro-user.html#wip[WIP] state of the change has changed.
+Sent when the link:intro-user.html#wip[WIP] state of the change has changed.
type:: wip-state-changed
@@ -277,7 +277,7 @@
=== Private State Changed
-Sent when the the link:intro-user.html#private-changes[private] state of the
+Sent when the link:intro-user.html#private-changes[private] state of the
change has changed.
type:: private-state-changed
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 3c556c4..cadab83 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -7,8 +7,9 @@
[NOTE]
The contents of the `etc/gerrit.config` file are cached at startup
-by Gerrit. If you modify any properties in this file, Gerrit needs
-to be restarted before it will use the new values.
+by Gerrit. For most properties, if they are modified in this file, Gerrit
+needs to be restarted before it will use the new values. Some properties
+support being link:#reloadConfig[`reloaded`]' without restart.
Sample `etc/gerrit.config`:
----
@@ -19,6 +20,14 @@
directory = /var/cache/gerrit
----
+[[reloadConfig]]
+=== Reload `etc/gerrit.config`
+Some properties support being reloaded without restart when a `reload config`
+command is issued through link:cmd-reload-config.html[`SSH`] or the
+link:rest-api-config.html#reload-config[`REST API`]. If a property supports
+this it is specified in the documentation for the property below.
+
+
[[accountPatchReviewDb]]
=== Section accountPatchReviewDb
@@ -127,6 +136,8 @@
This setting only applies for adding reviewers in the Gerrit Web UI,
but is ignored when adding reviewers with the
link:cmd-set-reviewers.html[set-reviewers] command.
++
+This value supports link:#reloadConfig[configuration reloads].
[[addreviewer.maxAllowed]]addreviewer.maxAllowed::
+
@@ -137,6 +148,8 @@
be added at once by adding a group as reviewer.
+
Default is 20.
++
+This value supports link:#reloadConfig[configuration reloads].
[[addReviewer.baseWeight]]addReviewer.baseWeight::
+
@@ -1245,6 +1258,14 @@
+
The default limit is 1024kB.
+[[change.strictLabels]]change.strictLabels::
++
+Reject invalid label votes: invalid labels or invalid values. This
+configuration option is provided for backwards compaitbility and may
+be removed in future gerrit versions.
++
+Default is false.
+
[[change.disablePrivateChanges]]change.disablePrivateChanges::
+
If set to true, users are not allowed to create private changes.
@@ -1320,6 +1341,10 @@
configuration 'tracker' uses raw HTML to more precisely control
how the replacement is displayed to the user.
+commentlinks supports link:#reloadConfig[configuration reloads]. Though a
+link:cmd-flush-caches.html[flush-caches] of "projects" is needed for the
+commentlinks to be immediately available in the UI.
+
----
[commentlink "changeid"]
match = (I[0-9a-f]{8,40})
@@ -2167,6 +2192,19 @@
Defaults to GWT (if GWT is enabled) or POLYGERRIT (if POLYGERRIT is
enabled and GWT is disabled)
+[[gerrit.serverId]]gerrit.serverId::
++
+Used by NoteDb to, amongst other things, identify author identities from
+per-server specific account IDs.
++
+If this value is not set on startup it is automatically set to a random UUID.
++
+[NOTE]
+If this value doesn't match the serverId used when creating an already existing
+NoteDb, Gerrit will not be able to use that instance of NoteDb. The serverId
+used to create the NoteDb will show in the resulting exception message in case
+the value differs.
+
[[gitweb]]
=== Section gitweb
@@ -2686,7 +2724,9 @@
+
* `ELASTICSEARCH` look into link:#elasticsearch[Elasticsearch section]
+
-An link:http://www.elasticsearch.org/[Elasticsearch] index is used.
+An link:https://www.elastic.co/products/elasticsearch[Elasticsearch] index is
+used. Refer to the link:#elasticsearch[Elasticsearch section] for further
+configuration details.
+
By default, `LUCENE`.
@@ -2728,9 +2768,10 @@
+
When `index.type` is set to `ELASTICSEARCH`, this value should not exceed
the `index.max_result_window` value configured on the Elasticsearch
-server.
+server. If a value is not configured during site initialization, defaults to
+10000, which is the default value of `index.max_result_window` in Elasticsearch.
+
-Defaults to no limit.
+When `index.type` is set to `LUCENE`, defaults to no limit.
[[index.maxPages]]index.maxPages::
+
@@ -2896,8 +2937,8 @@
[[elasticsearch]]
=== Section elasticsearch
-WARNING: The Elasticsearch support is incomplete. Online reindexing
-is still considered as beta.
+WARNING: The Elasticsearch support has only been tested with Elasticsearch
+version 2.4.x. Support for other versions is not guaranteed.
Open and closed changes are indexed in a single index, separated
into types 'open_changes' and 'closed_changes' respectively.
@@ -3884,6 +3925,15 @@
+
Default is 1.
+[[execution.fanOutThreadPoolSize]]execution.fanOutThreadPoolSize::
++
+Maximum size of thread pool to on which a serving thread can fan-out
+work to parallelize it.
++
+When set to 0, a direct executor will be used.
++
+By default, 25 which means that formatting happens in the caller thread.
+
[[receiveemail]]
=== Section receiveemail
@@ -4509,6 +4559,8 @@
programmatic configuration.
+
By default, `true`.
++
+This value supports link:#reloadConfig[configuration reloads].
[[sshd.rekeyBytesLimit]]sshd.rekeyBytesLimit::
+
@@ -4536,6 +4588,8 @@
The maximum numbers of reviewers suggested.
+
By default 10.
++
+This value supports link:#reloadConfig[configuration reloads].
[[suggest.from]]suggest.from::
+
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index 6272b54..91e20cd 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -161,7 +161,9 @@
the parent definition must be redefined in the child.
To remove a label in a child project, add an empty label with the same
-name as in the parent.
+name as in the parent. This will override the parent label with
+a label containing the defaults (`function = MaxWithBlock`,
+`defaultValue = 0` and no further allowed values)
[[label_layout]]
=== Layout
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 3e52f16..89c4b84 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -10,6 +10,9 @@
* Python 2 or 3
* Node.js
* link:https://www.bazel.io/versions/master/docs/install.html[Bazel]
+* Maven
+* zip, unzip
+* gcc
[[build]]
== Building on the Command Line
@@ -390,6 +393,7 @@
build --experimental_local_disk_cache_path=/home/<user>/.gerritcodereview/bazel-cache/cas
build --experimental_local_disk_cache
build --experimental_strict_action_env
+build --action_env=PATH
----
[NOTE] `experimental_local_disk_cache_path` must be absolute path. Expansion of `~` is
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index ee07880..c6cadbb 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -164,7 +164,7 @@
To format Java source code, Gerrit uses the
link:https://github.com/google/google-java-format[`google-java-format`]
-tool (version 1.3), and to format Bazel BUILD and WORKSPACE files the
+tool (version 1.5), and to format Bazel BUILD and WORKSPACE files the
link:https://github.com/bazelbuild/buildifier[`buildifier`] tool (version 0.6.0).
These tools automatically apply format according to the style guides; this
streamlines code review by reducing the need for time-consuming, tedious,
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 6ce7f1f..f5042a7 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -810,7 +810,7 @@
and `Edit Config` buttons on the project screen, and the `Follow-Up`
button on the change screen).
-- [[publish-comments-on-push]]`Publish Draft Comments When a Change Is Updated by Push`:
+- [[publish-comments-on-push]]`Publish comments on push`:
+
Whether to publish any outstanding draft comments by default when pushing
updates to open changes. This preference just sets the default; the behavior can
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 7360bd4..3b8a8cb 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -185,14 +185,16 @@
[[requirement]]
== requirement
-Information about a requirement (not met) in order to submit a change.
+Information about a requirement in order to submit a change.
-shortReason:: A short description of the requirement (a hint).
+fallbackText:: A human readable description of the requirement.
-fullReason:: A longer and descriptive message explaining what needs to
-be changed to meet the requirement.
+type:: Alphanumerical (plus hyphens or underscores) string to identify what the requirement is and
+why it was triggered. Can be seen as a class: requirements sharing the same type were created for a
+similar reason, and the data structure will follow one set of rules.
-label:: (Optional) The name of the linked label, if set by a pre-submit rule.
+data:: (Optional) Additional key-value data linked to this requirement. This is used in templates to
+render rich status messages.
[[label]]
== label
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 229c463..fa65a0b 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -55,6 +55,12 @@
* `http/server/rest_api/server_latency`: REST API call latency by view.
* `http/server/rest_api/response_bytes`: Size of REST API response on network
(may be gzip compressed) by view.
+* `http/server/rest_api/change_json/to_change_info_latency`: Latency for
+toChangeInfo invocations in ChangeJson.
+* `http/server/rest_api/change_json/to_change_infos_latency`: Latency for
+toChangeInfos invocations in ChangeJson.
+* `http/server/rest_api/change_json/format_query_results_latency`: Latency for
+formatQueryResults invocations in ChangeJson.
=== Query
diff --git a/Documentation/pg-plugin-change-metadata-api.txt b/Documentation/pg-plugin-change-metadata-api.txt
new file mode 100644
index 0000000..8348da8
--- /dev/null
+++ b/Documentation/pg-plugin-change-metadata-api.txt
@@ -0,0 +1,16 @@
+= Gerrit Code Review - Change metadata plugin API
+
+This API is provided by
+link:pg-plugin-dev.html#change-metadata[plugin.changeMetadata()] and provides
+interface for customization and data updates of change metadata.
+
+== onLabelsChanged
+`changeMetadataApi.onLabelsChanged(callback)`
+
+.Params
+- *callback* function that's executed when labels changed on the server.
+Callback receives labels with scores applied to the change, map of the label
+names to link:rest-api-changes.html#label-info[LabelInfo] entries
+
+.Returns
+- `GrChangeMetadataApi` for chaining.
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index 580166a..c7aa57c 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -289,7 +289,18 @@
Note: TODO
-[plugin-repo]
+[[plugin-rest-api]]
+=== restApi
+`plugin.restApi(opt_prefix)`
+
+.Params:
+- (optional) URL prefix, for easy switching into plugin URL space,
+ e.g. `changes/1/revisions/1/cookbook~say-hello`
+
+.Returns:
+- Instance of link:pg-plugin-rest-api.html[GrPluginRestApi].
+
+[[plugin-repo]]
=== repo
`plugin.repo()`
@@ -341,6 +352,15 @@
Deprecated. Use link:#plugin-settings[`plugin.settings()`] instead.
+=== changeMetadata
+`plugin.changeMetadata()`
+
+.Params:
+- none
+
+.Returns:
+- Instance of link:pg-plugin-change-metadata-api.html[GrChangeMetadataApi].
+
=== theme
`plugin.theme()`
diff --git a/Documentation/pg-plugin-endpoints.txt b/Documentation/pg-plugin-endpoints.txt
index d3d0a8d..b77a66b 100644
--- a/Documentation/pg-plugin-endpoints.txt
+++ b/Documentation/pg-plugin-endpoints.txt
@@ -69,6 +69,11 @@
current revision displayed, an instance of
link:rest-api-changes.html#revision-info[RevisionInfo]
+* `labels`
++
+labels with scores applied to the change, map of the label names to
+link:rest-api-changes.html#label-info[LabelInfo] entries
+
=== robot-comment-controls
The `robot-comment-controls` extension point is located inside each comment
rendered on the diff page, and is only visible when the comment is a robot
@@ -118,4 +123,4 @@
This endpoint decorator wraps the voting buttons in the reply dialog.
=== header-title
-This endpoint wraps the title-text in the application header.
\ No newline at end of file
+This endpoint wraps the title-text in the application header.
diff --git a/Documentation/pg-plugin-rest-api.txt b/Documentation/pg-plugin-rest-api.txt
new file mode 100644
index 0000000..70487ef
--- /dev/null
+++ b/Documentation/pg-plugin-rest-api.txt
@@ -0,0 +1,104 @@
+= Gerrit Code Review - Repo admin customization API
+
+This API is provided by link:pg-plugin-dev.html#plugin-rest-api[plugin.restApi()]
+and provides interface for Gerrit REST API.
+
+== getLoggedIn
+`repoApi.getLoggedIn()`
+
+Get user logged in status.
+
+.Params
+- None
+
+.Returns
+- Promise<boolean>
+
+== getVersion
+`repoApi.getVersion()`
+
+Get server version.
+
+.Params
+- None
+
+.Returns
+- Promise<string>
+
+== get
+`repoApi.get(url)`
+
+Issues a GET REST API call to the URL, returns Promise that is resolved to
+parsed response on success. Returned Promise is rejected on network error.
+
+.Params
+- *url* String URL without base path or plugin prefix.
+
+.Returns
+- Promise<Object> Parsed response.
+
+== post
+`repoApi.post(url, opt_payload)`
+
+Issues a POST REST API call to the URL, returns Promise that is resolved to
+parsed response on success. Returned Promise is rejected on network error.
+
+.Params
+- *url* String URL without base path or plugin prefix.
+- *opt_payload* (optional) Object Payload to be sent with the request.
+
+.Returns
+- Promise<Object> Parsed response.
+
+== put
+`repoApi.put(url, opt_payload)`
+
+Issues a PUT REST API call to the URL, returns Promise that is resolved to
+parsed response on success. Returned Promise is rejected on network error.
+
+.Params
+- *url* String URL without base path or plugin prefix.
+- *opt_payload* (optional) Object Payload to be sent with the request.
+
+.Returns
+- Promise<Object> Parsed response.
+
+== delete
+`repoApi.delete(url)`
+
+Issues a DELETE REST API call to the URL, returns Promise that is resolved to
+parsed response on HTTP 204, and rejected otherwise.
+
+.Params
+- *url* String URL without base path or plugin prefix.
+
+.Returns
+- Promise<Response> Fetch API's Response object.
+
+== send
+`repoApi.send(method, url, opt_payload)`
+
+Send payload and parse the response, if request succeeds. Returned Promise is
+rejected with detailed message or HTTP error code on network error.
+
+.Params
+- *method* String HTTP method.
+- *url* String URL without base path or plugin prefix.
+- *opt_payload* (optional) Object Respected for POST and PUT only.
+
+.Returns
+- Promise<Object> Parsed response.
+
+== fetch
+`repoApi.fetch(method, url, opt_payload)`
+
+Send payload and return native Response. This method is for low-level access, to
+implement custom error handling and parsing.
+
+.Params
+- *method* String HTTP method.
+- *url* String URL without base path or plugin prefix.
+- *opt_payload* (optional) Object Respected for POST and PUT only.
+
+.Returns
+- Promise<Response> Fetch API's Response object.
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 386e2d6..4e3c428 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -901,7 +901,10 @@
findall(X, gerrit:commit_label(label(Category,X),R),Z),
sum_list(Z, Sum),
Sum >= Min, !,
- P = [label(Category,ok(R)) | In].
+ gerrit:commit_label(label(Category, V), U),
+ V >= 1,
+ !,
+ P = [label(Category,ok(U)) | In].
add_category_min_score(In, Category,Min,P) :-
P = [label(Category,need(Min)) | In].
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 3c0f1e0..025b29d 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1765,6 +1765,89 @@
HTTP/1.1 204 No Content
----
+[[list-contributor-agreements]]
+=== List Contributor Agreements
+--
+'GET /accounts/link:#account-id[\{account-id\}]/agreements'
+--
+
+Gets a list of the user's signed contributor agreements.
+
+.Request
+----
+ GET /a/accounts/self/agreements HTTP/1.0
+----
+
+As response the user's signed agreements are returned as a list
+of link:#contributor-agreement-info[ContributorAgreementInfo] entities.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ [
+ {
+ "name": "Individual",
+ "description": "If you are going to be contributing code on your own, this is the one you want. You can sign this one online.",
+ "url": "static/cla_individual.html"
+ }
+ ]
+----
+
+[[sign-contributor-agreement]]
+=== Sign Contributor Agreement
+--
+'PUT /accounts/link:#account-id[\{account-id\}]/agreements'
+--
+
+Signs a contributor agreement.
+
+The contributor agreement must be provided in the request body as
+a link:#contributor-agreement-input[ContributorAgreementInput].
+
+.Request
+----
+ PUT /accounts/self/agreements HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "name": "Individual"
+ }
+----
+
+As response the contributor agreement name is returned.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ "Individual"
+----
+
+[[index-account]]
+=== Index Account
+--
+'POST /accounts/link:#account-id[\{account-id\}]/index'
+--
+
+Adds or updates the account in the secondary index.
+
+.Request
+----
+ POST /accounts/1000096/index HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
[[default-star-endpoints]]
== Default Star Endpoints
@@ -1981,89 +2064,6 @@
]
----
-[[list-contributor-agreements]]
-=== List Contributor Agreements
---
-'GET /accounts/link:#account-id[\{account-id\}]/agreements'
---
-
-Gets a list of the user's signed contributor agreements.
-
-.Request
-----
- GET /a/accounts/self/agreements HTTP/1.0
-----
-
-As response the user's signed agreements are returned as a list
-of link:#contributor-agreement-info[ContributorAgreementInfo] entities.
-
-.Response
-----
- HTTP/1.1 200 OK
- Content-Disposition: attachment
- Content-Type: application/json; charset=UTF-8
-
- )]}'
- [
- {
- "name": "Individual",
- "description": "If you are going to be contributing code on your own, this is the one you want. You can sign this one online.",
- "url": "static/cla_individual.html"
- }
- ]
-----
-
-[[sign-contributor-agreement]]
-=== Sign Contributor Agreement
---
-'PUT /accounts/link:#account-id[\{account-id\}]/agreements'
---
-
-Signs a contributor agreement.
-
-The contributor agreement must be provided in the request body as
-a link:#contributor-agreement-input[ContributorAgreementInput].
-
-.Request
-----
- PUT /accounts/self/agreements HTTP/1.0
- Content-Type: application/json; charset=UTF-8
-
- {
- "name": "Individual"
- }
-----
-
-As response the contributor agreement name is returned.
-
-.Response
-----
- HTTP/1.1 200 OK
- Content-Disposition: attachment
- Content-Type: application/json; charset=UTF-8
-
- )]}'
- "Individual"
-----
-
-[[index-account]]
-=== Index Account
---
-'POST /accounts/link:#account-id[\{account-id\}]/index'
---
-
-Adds or updates the account in the secondary index.
-
-.Request
-----
- POST /accounts/1000096/index HTTP/1.0
-----
-
-.Response
-----
- HTTP/1.1 204 No Content
-----
-
[[ids]]
== IDs
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 8f889ac..9a09836 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -5642,6 +5642,9 @@
Actions the caller might be able to perform on this revision. The
information is a map of view name to link:#action-info[ActionInfo]
entities.
+|`requirements` |optional|
+List of the link:rest-api-changes.html#requirement[requirements] to be met before this change
+can be submitted.
|`labels` |optional|
The labels of the change as a map that maps the label names to
link:#label-info[LabelInfo] entries. +
@@ -6591,6 +6594,32 @@
oldest. Empty if there are no related changes.
|===========================
+
+[[requirement]]
+=== Requirement
+The `Requirement` entity contains information about a requirement relative to a change.
+
+type:: Alphanumerical (plus hyphens or underscores) string to identify what the requirement is and
+why it was triggered. Can be seen as a class: requirements sharing the same type were created for a
+similar reason, and the data structure will follow one set of rules.
+
+data:: (Optional) Additional key-value data linked to this requirement. This is used in templates to
+render rich status messages.
+
+[options="header",cols="1,^1,5"]
+|===========================
+|Field Name | |Description
+|`status` | | Status of the requirement. Can be either `OK`, `NOT_READY` or `RULE_ERROR`.
+|`fallbackText` | | A human readable reason
+|`type` | |
+Alphanumerical (plus hyphens or underscores) string to identify what the requirement is and why it
+was triggered. Can be seen as a class: requirements sharing the same type were created for a similar
+reason, and the data structure will follow one set of rules.
+|`data` |optional|
+Holds custom key-value strings, used in templates to render richer status messages
+|===========================
+
+
[[restore-input]]
=== RestoreInput
The `RestoreInput` entity contains information for restoring a change.
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt
index 3d18abb..59a4608 100644
--- a/Documentation/rest-api-config.txt
+++ b/Documentation/rest-api-config.txt
@@ -194,6 +194,51 @@
----
+[[reload-config]]
+=== Reload Config
+--
+'POST /config/server/reload'
+--
+
+Reloads the gerrit.config configuration.
+
+Not all configuration value can be picked up by this command. Which config
+sections and values that are supported is documented here:
+link:config-gerrit.html[Configuration]
+
+_The output shows only modified config values that are picked up by Gerrit
+and applied._
+
+If a config entry is added or removed from gerrit.config, but still brings
+no effect due to a matching default value, no output for this entry is shown.
+
+.Request
+----
+ POST /config/server/reload HTTP/1.0
+----
+
+As result a link:#config-update-info[ConfigUpdateInfo] entity is returned that
+contains information about how the updated config entries were handled.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "rejected": [],
+ "applied": [
+ {
+ "config_key": "addreviewer.maxAllowed",
+ "old_value": "20",
+ "new_value": "15"
+ }
+ ]
+ }
+----
+
+
[[confirm-email]]
=== Confirm Email
--
@@ -1609,6 +1654,39 @@
|`message` |Message describing the consistency problem.
|======================
+[[config-update-info]]
+=== ConfigUpdateInfo
+The entity describes the result of a reload of gerrit.config.
+
+If a changed config value is missing from the `applied` and the `rejected`
+lists there are no guarantees to whether they have or have not taken effect.
+
+[options="header",cols="1,6"]
+|======================
+|Field Name|Description
+|`applied` |A list of link:#config-update-entry-info[ConfigUpdateEntryInfos]
+describing the applied configuration changes. +
+Every config value change representation present in this list is guaranteed to
+have taken effect.
+|`rejected` |A list of link:#config-update-entry-info[ConfigUpdateEntryInfos]
+describing the rejected configuration changes. +
+Every config value change representation present in this list is guaranteed not
+to have taken effect.
+|======================
+
+[[config-update-entry-info]]
+=== ConfigUpdateEntryInfo
+The entity describes an updated config value.
+
+[options="header",cols="1,6"]
+|======================
+|Field Name|Description
+|`config_key` |The config key that contains the value.
+|`old_value` |The old config value. +
+Missing if value was not previously configured.
+|`new_value` |The new config value, picked up after reload.
+|======================
+
[[download-info]]
=== DownloadInfo
The `DownloadInfo` entity contains information about supported download
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 0ae3a64..bc5a3c6 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1311,7 +1311,7 @@
--
Runs access checks for other users. This requires the
-link:access-control.html#capability_administrateServer[Administrate Server]
+link:access-control.html#capability_viewAccess[View Access]
global capability.
Input for the access checks that should be run must be provided in
@@ -1345,6 +1345,14 @@
}
----
+This endpoint can also be accessed as a GET request, using the query
+parameters `perm`, `account` and `ref`, for example:
+
+----
+ GET /projects/MyProject/check.access?account=10024&ref=refs/heads/secret/bla
+----
+
+
[[index]]
=== Index all changes in a project
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 2ad8cbd..bebe81b 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -31,13 +31,15 @@
|Full or abbreviated Change-Id | Ic0ff33
|Full or abbreviated commit SHA-1 | d81b32ef
|Email address | user@example.com
-|Approval requirement | Code-Review>=+2, Verified=1
|=============================================================
For change searches (i.e. those using a numerical id, Change-Id, or commit
SHA1), if the search results in a single change that change will be
presented instead of a list.
+For more predictable results, use explicit search operators as described
+in the following section.
+
[[search-operators]]
== Search Operators
@@ -338,6 +340,11 @@
True on any change where the current user is a reviewer.
Same as `reviewer:self`.
+is:cc::
++
+True on any change where the current user is in CC.
+Same as `cc:self`.
+
is:open, is:pending::
+
True if the change is open.
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index 13d0755..a2a080b 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -27,7 +27,7 @@
.. a url that starts with the link:config-gerrit.html#gerrit.canonicalWebUrl[`gerrit.canonicalWebUrl`]
When a commit in a project is merged, Gerrit checks for superprojects
-that are subscribed to the the project and automatically updates those
+that are subscribed to the project and automatically updates those
superprojects with a commit that updates the gitlink for the project.
This feature is enabled by default and can be disabled
diff --git a/WORKSPACE b/WORKSPACE
index d782c23..4d1f81b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -415,10 +415,18 @@
sha1 = "430b2fc839b5de1f3643b528853d5cf26096c1de",
)
+AUTO_VALUE_VERSION = "1.6"
+
maven_jar(
name = "auto_value",
- artifact = "com.google.auto.value:auto-value:1.5.4",
- sha1 = "65183ddd1e9542d69d8f613fdae91540d04e1476",
+ artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION,
+ sha1 = "a3b1b1404f8acaa88594a017185e013cd342c9a8",
+)
+
+maven_jar(
+ name = "auto_value_annotations",
+ artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION,
+ sha1 = "da725083ee79fdcd86d9f3d8a76e38174a01892a",
)
# Transitive dependency of commons-compress
@@ -444,12 +452,6 @@
)
maven_jar(
- name = "lucene_codecs",
- artifact = "org.apache.lucene:lucene-codecs:" + LUCENE_VERS,
- sha1 = "afdad570668469b1734fbd32b8f98561561bed48",
-)
-
-maven_jar(
name = "backward_codecs",
artifact = "org.apache.lucene:lucene-backward-codecs:" + LUCENE_VERS,
sha1 = "a933f42e758c54c43083398127ea7342b54d8212",
@@ -486,12 +488,6 @@
)
maven_jar(
- name = "lucene_sandbox",
- artifact = "org.apache.lucene:lucene-sandbox:" + LUCENE_VERS,
- sha1 = "49498bbb2adc333e98bdca4bf6170ae770cbad11",
-)
-
-maven_jar(
name = "lucene_spatial",
artifact = "org.apache.lucene:lucene-spatial:" + LUCENE_VERS,
sha1 = "0217d302dc0ef4d9b8b475ffe327d83c1e0ceba5",
@@ -566,17 +562,17 @@
maven_jar(
name = "blame_cache",
- artifact = "com/google/gitiles:blame-cache:0.2-5",
+ artifact = "com/google/gitiles:blame-cache:0.2-6",
attach_source = False,
repository = GERRIT,
- sha1 = "50861b114350c598579ba66f99285e692e3c8d45",
+ sha1 = "64827f1bc2cbdbb6515f1d29ce115db94c03bb6a",
)
# Keep this version of Soy synchronized with the version used in Gitiles.
maven_jar(
name = "soy",
- artifact = "com.google.template:soy:2018-01-03",
- sha1 = "62089a55675f338bdfb41fba1b29fe610f654b4d",
+ artifact = "com.google.template:soy:2018-03-14",
+ sha1 = "76a1322705ba5a6d6329ee26e7387417725ce4b3",
)
maven_jar(
@@ -967,12 +963,6 @@
sha1 = "84ccf145ac2215e6bfa63baa3101c0af41017cfc",
)
-maven_jar(
- name = "jna",
- artifact = "net.java.dev.jna:jna:4.1.0",
- sha1 = "1c12d070e602efd8021891cdd7fd18bc129372d4",
-)
-
JACKSON_VERSION = "2.8.9"
maven_jar(
@@ -982,18 +972,18 @@
)
maven_jar(
- name = "jackson_dataformat_smile",
- artifact = "com.fasterxml.jackson.dataformat:jackson-dataformat-smile:" + JACKSON_VERSION,
- sha1 = "d36cbae6b06ac12fca16fda403759e479316141b",
-)
-
-maven_jar(
name = "jackson_dataformat_cbor",
artifact = "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:" + JACKSON_VERSION,
sha1 = "93242092324cad33d777e06c0515e40a6b862659",
)
maven_jar(
+ name = "jackson_dataformat_smile",
+ artifact = "com.fasterxml.jackson.dataformat:jackson-dataformat-smile:" + JACKSON_VERSION,
+ sha1 = "d36cbae6b06ac12fca16fda403759e479316141b",
+)
+
+maven_jar(
name = "httpasyncclient",
artifact = "org.apache.httpcomponents:httpasyncclient:4.1.2",
sha1 = "95aa3e6fb520191a0970a73cf09f62948ee614be",
@@ -1005,13 +995,6 @@
sha1 = "a8c5e3c3bfea5ce23fb647c335897e415eb442e3",
)
-maven_jar(
- name = "httpcore_niossl",
- artifact = "org.apache.httpcomponents:httpcore-niossl:4.0-alpha6",
- attach_source = False,
- sha1 = "9c662e7247ca8ceb1de5de629f685c9ef3e4ab58",
-)
-
load("//tools/bzl:js.bzl", "npm_binary", "bower_archive")
npm_binary(
diff --git a/contrib/git-push-review b/contrib/git-push-review
index 87eaa4c..b995fc2 100755
--- a/contrib/git-push-review
+++ b/contrib/git-push-review
@@ -50,6 +50,10 @@
help='reviewer names or aliases, or #hashtags')
p.add_argument('-t', '--topic', default='', metavar='TOPIC',
help='topic for new changes')
+ p.add_argument('-e', '--edit', action='store_true',
+ help='upload as change edit')
+ p.add_argument('-w', '--wip', action='store_true', help='upload as WIP')
+ p.add_argument('-y', '--ready', action='store_true', help='set ready')
p.add_argument('--dry-run', action='store_true',
help='dry run, print git command and exit')
args = p.parse_args()
@@ -77,7 +81,22 @@
opts['t'].extend(t[1:] for t in args.args if is_hashtag(t))
if args.topic:
opts['topic'].append(args.topic)
- opts_str = ','.join('%s=%s' % (k, v) for k in opts for v in opts[k])
+ if args.edit:
+ opts['edit'].append(True)
+ if args.wip:
+ opts['wip'].append(True)
+ if args.ready:
+ opts['ready'].append(True)
+
+ opts_strs = []
+ for k in opts:
+ for v in opts[k]:
+ if v == True:
+ opts_strs.append(k)
+ elif v != False:
+ opts_strs.append('%s=%s' % (k, v))
+
+ opts_str = ','.join(opts_strs)
if opts_str:
opts_str = '%' + opts_str
diff --git a/contrib/gitiles b/contrib/gitiles
new file mode 100755
index 0000000..3e603b9
--- /dev/null
+++ b/contrib/gitiles
@@ -0,0 +1,84 @@
+#!/bin/bash
+#
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+usage() {
+ me=`basename "$0"`
+ echo >&2 "Usage: $me [open] [-b branch] [path]"
+ exit 1
+}
+
+cmd_open() {
+ case "$(uname)" in
+ Darwin)
+ echo "open"
+ ;;
+ Linux)
+ echo "xdg-open"
+ ;;
+
+ *)
+ echo >&2 "Don't know how to open URLs on $(uname)"
+ exit 1
+ esac
+}
+
+URL=$(git config --get gitiles.url)
+
+if test -z "$URL" ; then
+ echo >&2 "gitiles.url must be set in .git/config"
+ exit 1
+fi
+
+while test $# -gt 0 ; do
+ case "$1" in
+ open)
+ CMD=$(cmd_open)
+ shift
+ ;;
+ -b|--branch)
+ shift
+ B=$1
+ shift
+ ;;
+ -h|--help)
+ usage
+ ;;
+
+ *)
+ P=$1
+ shift
+ esac
+done
+
+if test -z "$CMD" ; then
+ CMD=echo
+fi
+
+if test -z "$B" ; then
+ B=$(git rev-parse HEAD)
+fi
+
+URL="$URL/+/$B"
+
+if test -z "$P" ; then
+ P=$(git rev-parse --show-prefix)
+elif test ${P:0:2} = "./" ; then
+ P=$(git rev-parse --show-prefix)${P:2}
+fi
+
+URL="$URL/$P"
+
+$CMD $URL
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index a975b29..ca7cc27 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -38,7 +38,7 @@
showLegacycidInChangeTable = Show Change Number In Changes Table
muteCommonPathPrefixes = Mute Common Path Prefixes In File List
signedOffBy = Insert Signed-off-by Footer For Inline Edit Changes
-publishCommentsOnPush = Publish Draft Comments When a Change Is Updated by Push
+publishCommentsOnPush = Publish Comments On Push
myMenu = My Menu
myMenuInfo = \
Menu items for the 'My' top level menu. \
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index b57545b..cb6fe28 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
@@ -1259,7 +1259,7 @@
}
private void renderSubmitType(Change.Status status, boolean canSubmit, SubmitType submitType) {
- if (canSubmit && status == Change.Status.NEW) {
+ if (canSubmit && status == Change.Status.NEW && !changeInfo.isWorkInProgress()) {
statusText.setInnerText(
changeInfo.mergeable() ? Util.C.readyToSubmit() : Util.C.mergeConflict());
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 9860cb2..2d5a9f9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -8,7 +8,7 @@
notCurrent = Not Current
changeEdit = Change Edit
isPrivate = (Private)
-isWorkInProgress = (WorkInProgress)
+isWorkInProgress = (Work in Progress)
myDashboardTitle = My Reviews
unknownDashboardTitle = Code Review Dashboard
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 425fe69..caea87e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -240,6 +240,11 @@
row,
C_STATUS,
Util.toLongString(status) + (c.isPrivate() ? (" " + Util.C.isPrivate()) : ""));
+ } else if (c.isWorkInProgress()) {
+ table.setText(
+ row,
+ C_STATUS,
+ Util.C.workInProgress() + (c.isPrivate() ? (" " + Util.C.isPrivate()) : ""));
} else if (!c.mergeable()) {
table.setText(
row,
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 8c38f5e..fbf3b84 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -27,7 +27,6 @@
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import static org.eclipse.jgit.lib.Constants.HEAD;
-import static org.eclipse.jgit.lib.Constants.R_TAGS;
import com.github.rholder.retry.BlockStrategy;
import com.google.common.base.Strings;
@@ -37,6 +36,7 @@
import com.google.common.jimfs.Jimfs;
import com.google.common.primitives.Chars;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
+import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.ContributorAgreement;
@@ -61,7 +61,6 @@
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.extensions.common.ActionInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeType;
import com.google.gerrit.extensions.common.DiffInfo;
@@ -131,7 +130,7 @@
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Provider;
-import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.KeyPair;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -253,6 +252,7 @@
@Inject protected MutableNotesMigration notesMigration;
@Inject protected ChangeNotes.Factory notesFactory;
@Inject protected BatchAbandon batchAbandon;
+ @Inject protected TestSshKeys sshKeys;
protected EventRecorder eventRecorder;
protected GerritServer server;
@@ -334,14 +334,14 @@
// Don't reset all refs so that refs/sequences/changes is not touched and change IDs are
// not reused.
.reset(allProjects, RefNames.REFS_CONFIG)
- // Don't reset group branches since this would make the groups inconsistent between
- // ReviewDb and NoteDb.
// Don't reset refs/sequences/accounts so that account IDs are not reused.
.reset(
allUsers,
RefNames.REFS_CONFIG,
RefNames.REFS_USERS + "*",
RefNames.REFS_EXTERNAL_IDS,
+ RefNames.REFS_GROUPNAMES,
+ RefNames.REFS_GROUPS + "*",
RefNames.REFS_STARRED_CHANGES + "*",
RefNames.REFS_DRAFT_COMMENTS + "*");
}
@@ -441,7 +441,9 @@
Context ctx = newRequestContext(admin);
atrScope.set(ctx);
- project = createProject(projectInput(description));
+ ProjectInput in = projectInput(description);
+ gApi.projects().create(in);
+ project = new Project.NameKey(in.name);
testRepo = cloneProject(project, getCloneAsAccount(description));
}
@@ -450,12 +452,13 @@
return null;
}
- protected void initSsh() throws JSchException {
+ protected void initSsh() throws Exception {
if (testRequiresSsh
&& SshMode.useSsh()
&& (adminSshSession == null || userSshSession == null)) {
// Create Ssh sessions
- GitUtil.initSsh(admin);
+ KeyPair adminKeyPair = sshKeys.getKeyPair(admin);
+ GitUtil.initSsh(adminKeyPair);
Context ctx = newRequestContext(user);
atrScope.set(ctx);
userSshSession = ctx.getSession();
@@ -472,6 +475,7 @@
return accountCreator.get(ann != null ? ann.cloneAs() : "admin");
}
+ /** Generate default project properties based on test description */
private ProjectInput projectInput(Description description) {
ProjectInput in = new ProjectInput();
TestProjectInput ann = description.getAnnotation(TestProjectInput.class);
@@ -493,6 +497,15 @@
return in;
}
+ /**
+ * Modify a project input before creating the initial test project.
+ *
+ * @param in input; may be modified in place.
+ */
+ protected void updateProjectInput(ProjectInput in) {
+ // Default implementation does nothing.
+ }
+
private static final Pattern UNSAFE_PROJECT_NAME = Pattern.compile("[^a-zA-Z0-9._/-]+");
protected Git git() {
@@ -534,12 +547,6 @@
}
protected Project.NameKey createProject(
- String nameSuffix, Project.NameKey parent, SubmitType submitType) throws RestApiException {
- // Default for createEmptyCommit should match TestProjectConfig.
- return createProject(nameSuffix, parent, true, submitType);
- }
-
- protected Project.NameKey createProject(
String nameSuffix, Project.NameKey parent, boolean createEmptyCommit, SubmitType submitType)
throws RestApiException {
ProjectInput in = new ProjectInput();
@@ -547,35 +554,14 @@
in.parent = parent != null ? parent.get() : null;
in.submitType = submitType;
in.createEmptyCommit = createEmptyCommit;
- return createProject(in);
- }
-
- private Project.NameKey createProject(ProjectInput in) throws RestApiException {
gApi.projects().create(in);
return new Project.NameKey(in.name);
}
- /**
- * Modify a project input before creating the initial test project.
- *
- * @param in input; may be modified in place.
- */
- protected void updateProjectInput(ProjectInput in) {
- // Default implementation does nothing.
- }
-
protected TestRepository<InMemoryRepository> cloneProject(Project.NameKey p) throws Exception {
return cloneProject(p, admin);
}
- protected TestRepository<InMemoryRepository> cloneProject(Project.NameKey p, String ref)
- throws Exception {
- TestRepository<InMemoryRepository> repo = cloneProject(p);
- GitUtil.fetch(repo, ref + ":" + ref);
- repo.reset(ref);
- return repo;
- }
-
protected TestRepository<InMemoryRepository> cloneProject(
Project.NameKey p, TestAccount testAccount) throws Exception {
return GitUtil.cloneProject(p, registerRepoConnection(p, testAccount));
@@ -706,10 +692,6 @@
return result;
}
- protected PushOneCommit.Result createChangeWithTopic() throws Exception {
- return createChangeWithTopic(testRepo, "topic", "message", "a.txt", "content\n");
- }
-
protected PushOneCommit.Result createChangeWithTopic(
TestRepository<InMemoryRepository> repo,
String topic,
@@ -722,10 +704,6 @@
repo, "refs/for/master/" + name(topic), commitMsg, fileName, content);
}
- protected PushOneCommit.Result createWorkInProgressChange() throws Exception {
- return pushTo("refs/for/master%wip");
- }
-
protected PushOneCommit.Result createChange(String subject, String fileName, String content)
throws Exception {
PushOneCommit push =
@@ -734,13 +712,6 @@
}
protected PushOneCommit.Result createChange(
- String subject, String fileName, String content, String topic) throws Exception {
- PushOneCommit push =
- pushFactory.create(db, admin.getIdent(), testRepo, subject, fileName, content);
- return push.to("refs/for/master/" + name(topic));
- }
-
- protected PushOneCommit.Result createChange(
TestRepository<?> repo,
String branch,
String subject,
@@ -752,10 +723,6 @@
return push.to("refs/for/" + branch + "/" + name(topic));
}
- protected BranchApi createBranch(String branch) throws Exception {
- return createBranch(new Branch.NameKey(project, branch));
- }
-
protected BranchApi createBranch(Branch.NameKey branch) throws Exception {
return gApi.projects()
.name(branch.getParentKey().get())
@@ -774,11 +741,7 @@
Chars.asList(new char[] {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'});
protected PushOneCommit.Result amendChange(String changeId) throws Exception {
- return amendChange(changeId, "refs/for/master");
- }
-
- protected PushOneCommit.Result amendChange(String changeId, String ref) throws Exception {
- return amendChange(changeId, ref, admin, testRepo);
+ return amendChange(changeId, "refs/for/master", admin, testRepo);
}
protected PushOneCommit.Result amendChange(
@@ -838,7 +801,7 @@
private Context newRequestContext(TestAccount account) {
return atrScope.newContext(
reviewDbProvider,
- new SshSession(server, account),
+ new SshSession(sshKeys, server, account),
identifiedUserFactory.create(account.getId()));
}
@@ -929,9 +892,10 @@
protected void allow(Project.NameKey p, String ref, String permission, AccountGroup.UUID id)
throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
- Util.allow(cfg, permission, id, ref);
- saveProjectConfig(p, cfg);
+ try (ProjectConfigUpdate u = updateProject(p)) {
+ Util.allow(u.getConfig(), permission, id, ref);
+ u.save();
+ }
}
protected void allowGlobalCapabilities(AccountGroup.UUID id, String... capabilityNames)
@@ -941,11 +905,12 @@
protected void allowGlobalCapabilities(AccountGroup.UUID id, Iterable<String> capabilityNames)
throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
- for (String capabilityName : capabilityNames) {
- Util.allow(cfg, capabilityName, id);
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ for (String capabilityName : capabilityNames) {
+ Util.allow(u.getConfig(), capabilityName, id);
+ }
+ u.save();
}
- saveProjectConfig(allProjects, cfg);
}
protected void removeGlobalCapabilities(AccountGroup.UUID id, String... capabilityNames)
@@ -955,11 +920,12 @@
protected void removeGlobalCapabilities(AccountGroup.UUID id, Iterable<String> capabilityNames)
throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
- for (String capabilityName : capabilityNames) {
- Util.remove(cfg, capabilityName, id);
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ for (String capabilityName : capabilityNames) {
+ Util.remove(u.getConfig(), capabilityName, id);
+ }
+ u.save();
}
- saveProjectConfig(allProjects, cfg);
}
protected void setUseContributorAgreements(InheritableBoolean value) throws Exception {
@@ -995,9 +961,10 @@
protected void deny(Project.NameKey p, String ref, String permission, AccountGroup.UUID id)
throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
- Util.deny(cfg, permission, id, ref);
- saveProjectConfig(p, cfg);
+ try (ProjectConfigUpdate u = updateProject(p)) {
+ Util.deny(u.getConfig(), permission, id, ref);
+ u.save();
+ }
}
protected PermissionRule block(String ref, String permission, AccountGroup.UUID id)
@@ -1008,30 +975,20 @@
protected PermissionRule block(
Project.NameKey project, String ref, String permission, AccountGroup.UUID id)
throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- PermissionRule rule = Util.block(cfg, permission, id, ref);
- saveProjectConfig(project, cfg);
- return rule;
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ PermissionRule rule = Util.block(u.getConfig(), permission, id, ref);
+ u.save();
+ return rule;
+ }
}
protected void blockLabel(
String label, int min, int max, AccountGroup.UUID id, String ref, Project.NameKey project)
throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.block(cfg, Permission.LABEL + label, min, max, id, ref);
- saveProjectConfig(project, cfg);
- }
-
- protected void saveProjectConfig(Project.NameKey p, ProjectConfig cfg) throws Exception {
- try (MetaDataUpdate md = metaDataUpdateFactory.create(p)) {
- md.setAuthor(identifiedUserFactory.create(admin.getId()));
- cfg.commit(md);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.block(u.getConfig(), Permission.LABEL + label, min, max, id, ref);
+ u.save();
}
- projectCache.evict(cfg.getProject());
- }
-
- protected void saveProjectConfig(ProjectConfig cfg) throws Exception {
- saveProjectConfig(project, cfg);
}
protected void grant(Project.NameKey project, String ref, String permission)
@@ -1108,12 +1065,6 @@
block(ref, Permission.READ, REGISTERED_USERS);
}
- protected void blockForgeCommitter(Project.NameKey project, String ref) throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.block(cfg, Permission.FORGE_COMMITTER, REGISTERED_USERS, ref);
- saveProjectConfig(project, cfg);
- }
-
protected PushOneCommit.Result pushTo(String ref) throws Exception {
PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
return push.to(ref);
@@ -1127,26 +1078,18 @@
gApi.changes().id(id).revision("current").review(ReviewInput.recommend());
}
- protected Map<String, ActionInfo> getActions(String id) throws Exception {
- return gApi.changes().id(id).revision(1).actions();
- }
-
- protected String getETag(String id) throws Exception {
- return gApi.changes().id(id).current().etag();
- }
-
- private static Iterable<String> changeIds(Iterable<ChangeInfo> changes) {
- return Iterables.transform(changes, i -> i.changeId);
- }
-
protected void assertSubmittedTogether(String chId, String... expected) throws Exception {
List<ChangeInfo> actual = gApi.changes().id(chId).submittedTogether();
SubmittedTogetherInfo info =
gApi.changes().id(chId).submittedTogether(EnumSet.of(NON_VISIBLE_CHANGES));
assertThat(info.nonVisibleChanges).isEqualTo(0);
- assertThat(changeIds(actual)).containsExactly((Object[]) expected).inOrder();
- assertThat(changeIds(info.changes)).containsExactly((Object[]) expected).inOrder();
+ assertThat(Iterables.transform(actual, i1 -> i1.changeId))
+ .containsExactly((Object[]) expected)
+ .inOrder();
+ assertThat(Iterables.transform(info.changes, i -> i.changeId))
+ .containsExactly((Object[]) expected)
+ .inOrder();
}
protected PatchSet getPatchSet(PatchSet.Id psId) throws OrmException {
@@ -1200,18 +1143,6 @@
return name;
}
- protected String createAccount(String name, String group) throws Exception {
- name = name(name);
- accountCreator.create(name, group);
- return name;
- }
-
- protected TestAccount createUniqueAccount(String userName, String fullName) throws Exception {
- String uniqueUserName = name(userName);
- String uniqueFullName = name(fullName);
- return accountCreator.create(uniqueUserName, uniqueUserName + "@example.com", uniqueFullName);
- }
-
protected RevCommit getHead(Repository repo, String name) throws Exception {
try (RevWalk rw = new RevWalk(repo)) {
Ref r = repo.exactRef(name);
@@ -1237,13 +1168,6 @@
return getRemoteHead(project, "master");
}
- protected void grantTagPermissions() throws Exception {
- grant(project, R_TAGS + "*", Permission.CREATE);
- grant(project, R_TAGS + "", Permission.DELETE);
- grant(project, R_TAGS + "*", Permission.CREATE_TAG);
- grant(project, R_TAGS + "*", Permission.CREATE_SIGNED_TAG);
- }
-
protected void assertMailReplyTo(Message message, String email) throws Exception {
assertThat(message.headers()).containsKey("Reply-To");
EmailHeader.String replyTo = (EmailHeader.String) message.headers().get("Reply-To");
@@ -1270,22 +1194,15 @@
ca.setDescription("description");
ca.setAgreementUrl("agreement-url");
- ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
- cfg.replace(ca);
- saveProjectConfig(allProjects, cfg);
- return ca;
- }
-
- protected BinaryResult submitPreview(String changeId) throws Exception {
- return gApi.changes().id(changeId).current().submitPreview();
- }
-
- protected BinaryResult submitPreview(String changeId, String format) throws Exception {
- return gApi.changes().id(changeId).current().submitPreview(format);
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ u.getConfig().replace(ca);
+ u.save();
+ return ca;
+ }
}
protected Map<Branch.NameKey, ObjectId> fetchFromSubmitPreview(String changeId) throws Exception {
- try (BinaryResult result = submitPreview(changeId)) {
+ try (BinaryResult result = gApi.changes().id(changeId).current().submitPreview()) {
return fetchFromBundles(result);
}
}
@@ -1406,14 +1323,6 @@
assertThat(contentEntry.skip).isNull();
}
- protected TestRepository<?> createProjectWithPush(
- String name, @Nullable Project.NameKey parent, SubmitType submitType) throws Exception {
- Project.NameKey project = createProject(name, parent, true, submitType);
- grant(project, "refs/heads/*", Permission.PUSH);
- grant(project, "refs/for/refs/heads/*", Permission.SUBMIT);
- return cloneProject(project);
- }
-
protected void assertPermitted(ChangeInfo info, String label, Integer... expected) {
assertThat(info.permittedLabels).isNotNull();
Collection<String> strs = info.permittedLabels.get(label);
@@ -1443,27 +1352,7 @@
}
}
- protected void assertLabelPermission(
- Project.NameKey project,
- GroupReference groupReference,
- String ref,
- boolean exclusive,
- String labelName,
- int min,
- int max)
- throws IOException {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- AccessSection accessSection = cfg.getAccessSection(ref);
- assertThat(accessSection).isNotNull();
-
- String permissionName = Permission.LABEL + labelName;
- Permission permission = accessSection.getPermission(permissionName);
- assertPermission(permission, permissionName, exclusive, labelName);
- assertPermissionRule(
- permission.getRule(groupReference), groupReference, Action.ALLOW, false, min, max);
- }
-
- private void assertPermission(
+ protected void assertPermission(
Permission permission,
String expectedName,
boolean expectedExclusive,
@@ -1474,7 +1363,7 @@
assertThat(permission.getLabel()).isEqualTo(expectedLabelName);
}
- private void assertPermissionRule(
+ protected void assertPermissionRule(
PermissionRule rule,
GroupReference expectedGroupReference,
Action expectedAction,
@@ -1533,15 +1422,21 @@
}
protected void assertNotifyTo(TestAccount expected) {
- assertNotifyTo(expected.emailAddress);
+ assertNotifyTo(expected.email, expected.fullName);
}
- protected void assertNotifyTo(Address expected) {
+ protected void assertNotifyTo(
+ com.google.gerrit.acceptance.testsuite.account.TestAccount expected) {
+ assertNotifyTo(expected.preferredEmail().orElse(null), expected.fullname().orElse(null));
+ }
+
+ private void assertNotifyTo(String expectedEmail, String expectedFullname) {
+ Address expectedAddress = new Address(expectedFullname, expectedEmail);
assertThat(sender.getMessages()).hasSize(1);
Message m = sender.getMessages().get(0);
- assertThat(m.rcpt()).containsExactly(expected);
+ assertThat(m.rcpt()).containsExactly(expectedAddress);
assertThat(((EmailHeader.AddressList) m.headers().get("To")).getAddressList())
- .containsExactly(expected);
+ .containsExactly(expectedAddress);
assertThat(m.headers().get("Cc").isEmpty()).isTrue();
}
@@ -1549,13 +1444,23 @@
assertNotifyCc(expected.emailAddress);
}
- protected void assertNotifyCc(Address expected) {
+ protected void assertNotifyCc(
+ com.google.gerrit.acceptance.testsuite.account.TestAccount expected) {
+ assertNotifyCc(expected.preferredEmail().orElse(null), expected.fullname().orElse(null));
+ }
+
+ protected void assertNotifyCc(String expectedEmail, String expectedFullname) {
+ Address expectedAddress = new Address(expectedFullname, expectedEmail);
+ assertNotifyCc(expectedAddress);
+ }
+
+ protected void assertNotifyCc(Address expectedAddress) {
assertThat(sender.getMessages()).hasSize(1);
Message m = sender.getMessages().get(0);
- assertThat(m.rcpt()).containsExactly(expected);
+ assertThat(m.rcpt()).containsExactly(expectedAddress);
assertThat(m.headers().get("To").isEmpty()).isTrue();
assertThat(((EmailHeader.AddressList) m.headers().get("Cc")).getAddressList())
- .containsExactly(expected);
+ .containsExactly(expectedAddress);
}
protected void assertNotifyBcc(TestAccount expected) {
@@ -1566,6 +1471,17 @@
assertThat(m.headers().get("Cc").isEmpty()).isTrue();
}
+ protected void assertNotifyBcc(
+ com.google.gerrit.acceptance.testsuite.account.TestAccount expected) {
+ assertThat(sender.getMessages()).hasSize(1);
+ Message m = sender.getMessages().get(0);
+ assertThat(m.rcpt())
+ .containsExactly(
+ new Address(expected.fullname().orElse(null), expected.preferredEmail().orElse(null)));
+ assertThat(m.headers().get("To").isEmpty()).isTrue();
+ assertThat(m.headers().get("Cc").isEmpty()).isTrue();
+ }
+
protected interface ProjectWatchInfoConfiguration {
void configure(ProjectWatchInfo pwi);
}
@@ -1632,10 +1548,6 @@
}
}
- protected RevCommit parseCurrentRevision(RevWalk rw, PushOneCommit.Result r) throws Exception {
- return parseCurrentRevision(rw, r.getChangeId());
- }
-
protected RevCommit parseCurrentRevision(RevWalk rw, String changeId) throws Exception {
return rw.parseCommit(
ObjectId.fromString(get(changeId, ListChangesOption.CURRENT_REVISION).currentRevision));
@@ -1649,27 +1561,59 @@
protected void configLabel(
Project.NameKey project, String label, LabelFunction func, LabelValue... value)
throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- LabelType labelType = category(label, value);
- labelType.setFunction(func);
- cfg.getLabelSections().put(labelType.getName(), labelType);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = category(label, value);
+ labelType.setFunction(func);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
}
protected void fail(@Nullable String format, Object... args) {
assert_().fail(format, args);
}
- protected void fail() {
- assert_().fail();
+ protected void enableCreateNewChangeForAllNotInTarget() throws Exception {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .getProject()
+ .setBooleanConfig(
+ BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
+ InheritableBoolean.TRUE);
+ u.save();
+ }
}
- protected void enableCreateNewChangeForAllNotInTarget() throws Exception {
- ProjectConfig config = projectCache.checkedGet(project).getConfig();
- config
- .getProject()
- .setBooleanConfig(
- BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET, InheritableBoolean.TRUE);
- saveProjectConfig(project, config);
+ protected ProjectConfigUpdate updateProject(Project.NameKey projectName) throws Exception {
+ return new ProjectConfigUpdate(projectName);
+ }
+
+ protected class ProjectConfigUpdate implements AutoCloseable {
+ private final ProjectConfig projectConfig;
+ private MetaDataUpdate metaDataUpdate;
+
+ private ProjectConfigUpdate(Project.NameKey projectName) throws Exception {
+ metaDataUpdate = metaDataUpdateFactory.create(projectName);
+ projectConfig = ProjectConfig.read(metaDataUpdate);
+ }
+
+ public ProjectConfig getConfig() {
+ return projectConfig;
+ }
+
+ public void save() throws Exception {
+ metaDataUpdate.setAuthor(identifiedUserFactory.create(admin.getId()));
+ projectConfig.commit(metaDataUpdate);
+ metaDataUpdate.close();
+ metaDataUpdate = null;
+ projectCache.evict(projectConfig.getProject());
+ }
+
+ @Override
+ public void close() {
+ if (metaDataUpdate != null) {
+ metaDataUpdate.close();
+ }
+ }
}
}
diff --git a/java/com/google/gerrit/acceptance/AccountCreator.java b/java/com/google/gerrit/acceptance/AccountCreator.java
index c6e03a8..1416797 100644
--- a/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -15,7 +15,6 @@
package com.google.gerrit.acceptance;
import static com.google.common.base.Preconditions.checkNotNull;
-import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
@@ -27,22 +26,15 @@
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.db.GroupsUpdate;
import com.google.gerrit.server.group.db.InternalGroupUpdate;
-import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.JSchException;
-import com.jcraft.jsch.KeyPair;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -57,29 +49,20 @@
private final Sequences sequences;
private final Provider<AccountsUpdate> accountsUpdateProvider;
- private final VersionedAuthorizedKeys.Accessor authorizedKeys;
private final GroupCache groupCache;
private final Provider<GroupsUpdate> groupsUpdateProvider;
- private final SshKeyCache sshKeyCache;
- private final boolean sshEnabled;
@Inject
AccountCreator(
Sequences sequences,
@ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider,
- VersionedAuthorizedKeys.Accessor authorizedKeys,
GroupCache groupCache,
- @ServerInitiated Provider<GroupsUpdate> groupsUpdateProvider,
- SshKeyCache sshKeyCache,
- @SshEnabled boolean sshEnabled) {
+ @ServerInitiated Provider<GroupsUpdate> groupsUpdateProvider) {
accounts = new HashMap<>();
this.sequences = sequences;
this.accountsUpdateProvider = accountsUpdateProvider;
- this.authorizedKeys = authorizedKeys;
this.groupCache = groupCache;
this.groupsUpdateProvider = groupsUpdateProvider;
- this.sshKeyCache = sshKeyCache;
- this.sshEnabled = sshEnabled;
}
public synchronized TestAccount create(
@@ -124,14 +107,7 @@
}
}
- KeyPair sshKey = null;
- if (sshEnabled && username != null) {
- sshKey = genSshKey();
- authorizedKeys.addKey(id, publicKey(sshKey, email));
- sshKeyCache.evict(username);
- }
-
- account = new TestAccount(id, username, email, fullName, sshKey, httpPass);
+ account = new TestAccount(id, username, email, fullName, httpPass);
if (username != null) {
accounts.put(username, account);
}
@@ -174,18 +150,6 @@
accounts.values().removeIf(a -> ids.contains(a.id));
}
- public static KeyPair genSshKey() throws JSchException {
- JSch jsch = new JSch();
- return KeyPair.genKeyPair(jsch, KeyPair.RSA);
- }
-
- public static String publicKey(KeyPair sshKey, String comment)
- throws UnsupportedEncodingException {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- sshKey.writePublicKey(out, comment);
- return out.toString(US_ASCII.name()).trim();
- }
-
private void addGroupMember(AccountGroup.UUID groupUuid, Account.Id accountId)
throws OrmException, IOException, NoSuchGroupException, ConfigInvalidException {
InternalGroupUpdate groupUpdate =
diff --git a/java/com/google/gerrit/acceptance/BUILD b/java/com/google/gerrit/acceptance/BUILD
index d89dc92..acd5130a 100644
--- a/java/com/google/gerrit/acceptance/BUILD
+++ b/java/com/google/gerrit/acceptance/BUILD
@@ -13,6 +13,7 @@
"//java/com/google/gerrit/extensions:api",
"//java/com/google/gerrit/extensions/common/testing:common-test-util",
"//java/com/google/gerrit/extensions/restapi/testing:restapi-test-util",
+ "//java/com/google/gerrit/git/testing",
"//java/com/google/gerrit/gpg/testing:gpg-test-util",
"//java/com/google/gerrit/httpd",
"//java/com/google/gerrit/index",
@@ -72,12 +73,14 @@
"//java/com/google/gerrit/pgm:daemon",
"//java/com/google/gerrit/pgm/http/jetty",
"//java/com/google/gerrit/pgm/util",
+ "//java/com/google/gerrit/server/group/testing",
"//java/com/google/gerrit/server/project/testing:project-test-util",
"//java/com/google/gerrit/testing:gerrit-test-util",
"//lib:jimfs",
"//lib:truth",
"//lib:truth-java8-extension",
"//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/httpcomponents:fluent-hc",
"//lib/httpcomponents:httpclient",
"//lib/httpcomponents:httpcore",
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index f561f32..5699e3f 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -22,6 +22,8 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperationsImpl;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.lucene.LuceneIndexModule;
@@ -430,6 +432,7 @@
protected void configure() {
bindConstant().annotatedWith(SshEnabled.class).to(daemon.getEnableSshd());
bind(AccountCreator.class);
+ bind(AccountOperations.class).to(AccountOperationsImpl.class);
factory(PushOneCommit.Factory.class);
install(InProcessProtocol.module());
install(new NoSshModule());
diff --git a/java/com/google/gerrit/acceptance/GitUtil.java b/java/com/google/gerrit/acceptance/GitUtil.java
index e11651f..cdfdae7 100644
--- a/java/com/google/gerrit/acceptance/GitUtil.java
+++ b/java/com/google/gerrit/acceptance/GitUtil.java
@@ -19,10 +19,12 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
+import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.reviewdb.client.Project;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.KeyPair;
import com.jcraft.jsch.Session;
import java.io.IOException;
import java.util.List;
@@ -56,7 +58,7 @@
private static final AtomicInteger testRepoCount = new AtomicInteger();
private static final int TEST_REPO_WINDOW_DAYS = 2;
- public static void initSsh(TestAccount a) {
+ public static void initSsh(KeyPair keyPair) {
final Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
JSch.setConfig(config);
@@ -70,7 +72,8 @@
protected void configure(Host hc, Session session) {
try {
final JSch jsch = getJSch(hc, FS.DETECTED);
- jsch.addIdentity("KeyPair", a.privateKey(), a.sshKey.getPublicKeyBlob(), null);
+ jsch.addIdentity(
+ "KeyPair", TestSshKeys.privateKey(keyPair), keyPair.getPublicKeyBlob(), null);
} catch (JSchException e) {
throw new RuntimeException(e);
}
diff --git a/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java b/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java
index 62cc8ce..7e50b83 100644
--- a/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/LightweightPluginDaemonTest.java
@@ -33,7 +33,7 @@
protected TestServerPlugin plugin;
@Before
- public void setUp() throws Exception {
+ public void setUpTestPlugin() throws Exception {
TestPlugin testPlugin = getTestPlugin(getClass());
String name = testPlugin.name();
plugin =
@@ -52,7 +52,7 @@
}
@After
- public void tearDown() {
+ public void tearDownTestPlugin() {
if (plugin != null) {
// plugin will be null if the plugin test requires ssh, but the command
// line flag says we are running tests without ssh as the assume()
diff --git a/java/com/google/gerrit/acceptance/ProjectResetter.java b/java/com/google/gerrit/acceptance/ProjectResetter.java
index 86718be..7d17285 100644
--- a/java/com/google/gerrit/acceptance/ProjectResetter.java
+++ b/java/com/google/gerrit/acceptance/ProjectResetter.java
@@ -24,13 +24,17 @@
import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupIncludeCache;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.RefState;
import com.google.gerrit.server.index.account.AccountIndexer;
+import com.google.gerrit.server.index.group.GroupIndexer;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.RefPatternMatcher;
import com.google.inject.Inject;
@@ -88,6 +92,9 @@
@Nullable private final AccountCreator accountCreator;
@Nullable private final AccountCache accountCache;
@Nullable private final AccountIndexer accountIndexer;
+ @Nullable private final GroupCache groupCache;
+ @Nullable private final GroupIncludeCache groupIncludeCache;
+ @Nullable private final GroupIndexer groupIndexer;
@Nullable private final ProjectCache projectCache;
@Inject
@@ -97,12 +104,18 @@
@Nullable AccountCreator accountCreator,
@Nullable AccountCache accountCache,
@Nullable AccountIndexer accountIndexer,
+ @Nullable GroupCache groupCache,
+ @Nullable GroupIncludeCache groupIncludeCache,
+ @Nullable GroupIndexer groupIndexer,
@Nullable ProjectCache projectCache) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
this.accountCreator = accountCreator;
this.accountCache = accountCache;
this.accountIndexer = accountIndexer;
+ this.groupCache = groupCache;
+ this.groupIncludeCache = groupIncludeCache;
+ this.groupIndexer = groupIndexer;
this.projectCache = projectCache;
}
@@ -113,6 +126,9 @@
accountCreator,
accountCache,
accountIndexer,
+ groupCache,
+ groupIncludeCache,
+ groupIndexer,
projectCache,
input.refsByProject);
}
@@ -139,12 +155,18 @@
@Inject private AllUsersName allUsersName;
@Inject @Nullable private AccountCreator accountCreator;
@Inject @Nullable private AccountCache accountCache;
+ @Inject @Nullable private GroupCache groupCache;
+ @Inject @Nullable private GroupIncludeCache groupIncludeCache;
+ @Inject @Nullable private GroupIndexer groupIndexer;
@Inject @Nullable private AccountIndexer accountIndexer;
@Inject @Nullable private ProjectCache projectCache;
private final Multimap<Project.NameKey, String> refsPatternByProject;
+
+ // State to which to reset to.
private final Multimap<Project.NameKey, RefState> savedRefStatesByProject;
+ // Results of the resetting
private Multimap<Project.NameKey, String> keptRefsByProject;
private Multimap<Project.NameKey, String> restoredRefsByProject;
private Multimap<Project.NameKey, String> deletedRefsByProject;
@@ -155,6 +177,9 @@
@Nullable AccountCreator accountCreator,
@Nullable AccountCache accountCache,
@Nullable AccountIndexer accountIndexer,
+ @Nullable GroupCache groupCache,
+ @Nullable GroupIncludeCache groupIncludeCache,
+ @Nullable GroupIndexer groupIndexer,
@Nullable ProjectCache projectCache,
Multimap<Project.NameKey, String> refPatternByProject)
throws IOException {
@@ -163,6 +188,9 @@
this.accountCreator = accountCreator;
this.accountCache = accountCache;
this.accountIndexer = accountIndexer;
+ this.groupCache = groupCache;
+ this.groupIndexer = groupIndexer;
+ this.groupIncludeCache = groupIncludeCache;
this.projectCache = projectCache;
this.refsPatternByProject = refPatternByProject;
this.savedRefStatesByProject = readRefStates();
@@ -265,8 +293,8 @@
private void evictCachesAndReindex() throws IOException {
evictAndReindexProjects();
evictAndReindexAccounts();
+ evictAndReindexGroups();
- // TODO(ekempin): Evict groups from cache if group refs were modified.
// TODO(ekempin): Reindex changes if starred-changes refs in All-Users were modified.
}
@@ -329,16 +357,47 @@
}
}
+ /** Evict groups that were modified. */
+ private void evictAndReindexGroups() throws IOException {
+ if (groupCache != null || groupIndexer != null) {
+ Set<AccountGroup.UUID> modifiedGroups =
+ new HashSet<>(groupUUIDs(restoredRefsByProject.get(allUsersName)));
+ Set<AccountGroup.UUID> deletedGroups =
+ new HashSet<>(groupUUIDs(deletedRefsByProject.get(allUsersName)));
+
+ // Evict and reindex all modified and deleted groups.
+ for (AccountGroup.UUID uuid : Sets.union(modifiedGroups, deletedGroups)) {
+ evictAndReindexGroup(uuid);
+ }
+ }
+ }
+
private void evictAndReindexAccount(Account.Id accountId) throws IOException {
if (accountCache != null) {
accountCache.evict(accountId);
}
-
+ if (groupIncludeCache != null) {
+ groupIncludeCache.evictGroupsWithMember(accountId);
+ }
if (accountIndexer != null) {
accountIndexer.index(accountId);
}
}
+ private void evictAndReindexGroup(AccountGroup.UUID uuid) throws IOException {
+ if (groupCache != null) {
+ groupCache.evict(uuid);
+ }
+
+ if (groupIncludeCache != null) {
+ groupIncludeCache.evictParentGroupsOf(uuid);
+ }
+
+ if (groupIndexer != null) {
+ groupIndexer.index(uuid);
+ }
+ }
+
private Set<Account.Id> accountIds(Collection<String> refs) {
return refs.stream()
.filter(r -> r.startsWith(REFS_USERS))
@@ -346,4 +405,12 @@
.filter(Objects::nonNull)
.collect(toSet());
}
+
+ private Set<AccountGroup.UUID> groupUUIDs(Collection<String> refs) {
+ return refs.stream()
+ .filter(RefNames::isRefsGroups)
+ .map(r -> AccountGroup.UUID.fromRef(r))
+ .filter(Objects::nonNull)
+ .collect(toSet());
+ }
}
diff --git a/java/com/google/gerrit/acceptance/SshSession.java b/java/com/google/gerrit/acceptance/SshSession.java
index f7369d7..9e515ca 100644
--- a/java/com/google/gerrit/acceptance/SshSession.java
+++ b/java/com/google/gerrit/acceptance/SshSession.java
@@ -16,32 +16,34 @@
import static com.google.common.base.Preconditions.checkState;
+import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.KeyPair;
import com.jcraft.jsch.Session;
-import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.util.Scanner;
public class SshSession {
+ private final TestSshKeys sshKeys;
private final InetSocketAddress addr;
private final TestAccount account;
private Session session;
private String error;
- public SshSession(GerritServer server, TestAccount account) {
+ public SshSession(TestSshKeys sshKeys, GerritServer server, TestAccount account) {
+ this.sshKeys = sshKeys;
this.addr = server.getSshdAddress();
this.account = account;
}
- public void open() throws JSchException {
+ public void open() throws Exception {
getSession();
}
@SuppressWarnings("resource")
- public String exec(String command, InputStream opt) throws JSchException, IOException {
+ public String exec(String command, InputStream opt) throws Exception {
ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
try {
channel.setCommand(command);
@@ -60,7 +62,7 @@
}
}
- public InputStream exec2(String command, InputStream opt) throws JSchException, IOException {
+ public InputStream exec2(String command, InputStream opt) throws Exception {
ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
channel.setCommand(command);
channel.setInputStream(opt);
@@ -69,7 +71,7 @@
return in;
}
- public String exec(String command) throws JSchException, IOException {
+ public String exec(String command) throws Exception {
return exec(command, null);
}
@@ -88,10 +90,12 @@
}
}
- private Session getSession() throws JSchException {
+ private Session getSession() throws Exception {
if (session == null) {
+ KeyPair keyPair = sshKeys.getKeyPair(account);
JSch jsch = new JSch();
- jsch.addIdentity("KeyPair", account.privateKey(), account.sshKey.getPublicKeyBlob(), null);
+ jsch.addIdentity(
+ "KeyPair", TestSshKeys.privateKey(keyPair), keyPair.getPublicKeyBlob(), null);
session =
jsch.getSession(account.username, addr.getAddress().getHostAddress(), addr.getPort());
session.setConfig("StrictHostKeyChecking", "no");
diff --git a/java/com/google/gerrit/acceptance/TestAccount.java b/java/com/google/gerrit/acceptance/TestAccount.java
index 3563ca1..094e8b0 100644
--- a/java/com/google/gerrit/acceptance/TestAccount.java
+++ b/java/com/google/gerrit/acceptance/TestAccount.java
@@ -19,8 +19,6 @@
import com.google.common.net.InetAddresses;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.mail.Address;
-import com.jcraft.jsch.KeyPair;
-import java.io.ByteArrayOutputStream;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.List;
@@ -45,31 +43,17 @@
public final String email;
public final Address emailAddress;
public final String fullName;
- public final KeyPair sshKey;
public final String httpPassword;
- TestAccount(
- Account.Id id,
- String username,
- String email,
- String fullName,
- KeyPair sshKey,
- String httpPassword) {
+ TestAccount(Account.Id id, String username, String email, String fullName, String httpPassword) {
this.id = id;
this.username = username;
this.email = email;
this.emailAddress = new Address(fullName, email);
this.fullName = fullName;
- this.sshKey = sshKey;
this.httpPassword = httpPassword;
}
- public byte[] privateKey() {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- sshKey.writePrivateKey(out);
- return out.toByteArray();
- }
-
public PersonIdent getIdent() {
return new PersonIdent(fullName, email);
}
diff --git a/java/com/google/gerrit/acceptance/testsuite/ThrowingFunction.java b/java/com/google/gerrit/acceptance/testsuite/ThrowingFunction.java
new file mode 100644
index 0000000..d41672a
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/ThrowingFunction.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite;
+
+@FunctionalInterface
+public interface ThrowingFunction<T, R> {
+
+ R apply(T value) throws Exception;
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java
new file mode 100644
index 0000000..58a00d0
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperations.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.account;
+
+import com.google.gerrit.reviewdb.client.Account;
+
+/**
+ * An aggregation of operations on accounts for test purposes.
+ *
+ * <p>To execute the operations, no Gerrit permissions are necessary.
+ *
+ * <p><strong>Note:</strong> This interface is not implemented using the REST or extension API.
+ * Hence, it cannot be used for testing those APIs.
+ */
+public interface AccountOperations {
+
+ /**
+ * Starts the fluent chain for a querying or modifying an account. Please see the methods of
+ * {@link MoreAccountOperations} for details on possible operations.
+ *
+ * @return an aggregation of operations on a specific account
+ */
+ MoreAccountOperations account(Account.Id accountId);
+
+ /**
+ * Starts the fluent chain to create an account. The returned builder can be used to specify the
+ * attributes of the new account. To create the account for real, {@link
+ * TestAccountCreation.Builder#create()} must be called.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * TestAccount createdAccount = accountOperations
+ * .newAccount()
+ * .username("janedoe")
+ * .preferredEmail("janedoe@example.com")
+ * .fullname("Jane Doe")
+ * .create();
+ * </pre>
+ *
+ * <p><strong>Note:</strong> If another account with the provided user name or preferred email
+ * address already exists, the creation of the account will fail.
+ *
+ * @return a builder to create the new account
+ */
+ TestAccountCreation.Builder newAccount();
+
+ /** An aggregation of methods on a specific account. */
+ interface MoreAccountOperations {
+
+ /**
+ * Checks whether the account exists.
+ *
+ * @return {@code true} if the account exists
+ */
+ boolean exists() throws Exception;
+
+ /**
+ * Retrieves the account.
+ *
+ * <p><strong>Note:</strong> This call will fail with an exception if the requested account
+ * doesn't exist. If you want to check for the existence of an account, use {@link #exists()}
+ * instead.
+ *
+ * @return the corresponding {@code TestAccount}
+ */
+ TestAccount get() throws Exception;
+
+ /**
+ * Starts the fluent chain to update an account. The returned builder can be used to specify how
+ * the attributes of the account should be modified. To update the account for real, {@link
+ * TestAccountUpdate.Builder#update()} must be called.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * TestAccount updatedAccount = accountOperations.forUpdate().status("on vacation").update();
+ * </pre>
+ *
+ * <p><strong>Note:</strong> The update will fail with an exception if the account to update
+ * doesn't exist. If you want to check for the existence of an account, use {@link #exists()}.
+ *
+ * @return a builder to update the account
+ */
+ TestAccountUpdate.Builder forUpdate();
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
new file mode 100644
index 0000000..3d741b0
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/account/AccountOperationsImpl.java
@@ -0,0 +1,167 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.account;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.Sequences;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.Accounts;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.InternalAccountUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * The implementation of {@code AccountOperations}.
+ *
+ * <p>There is only one implementation of {@code AccountOperations}. Nevertheless, we keep the
+ * separation between interface and implementation to enhance clarity.
+ */
+public class AccountOperationsImpl implements AccountOperations {
+ private final Accounts accounts;
+ private final AccountsUpdate accountsUpdate;
+ private final Sequences seq;
+
+ @Inject
+ public AccountOperationsImpl(
+ Accounts accounts, @ServerInitiated AccountsUpdate accountsUpdate, Sequences seq) {
+ this.accounts = accounts;
+ this.accountsUpdate = accountsUpdate;
+ this.seq = seq;
+ }
+
+ @Override
+ public MoreAccountOperations account(Account.Id accountId) {
+ return new MoreAccountOperationsImpl(accountId);
+ }
+
+ @Override
+ public TestAccountCreation.Builder newAccount() {
+ return TestAccountCreation.builder(this::createAccount);
+ }
+
+ private TestAccount createAccount(TestAccountCreation accountCreation) throws Exception {
+ AccountsUpdate.AccountUpdater accountUpdater =
+ (account, updateBuilder) ->
+ fillBuilder(updateBuilder, accountCreation, account.getAccount().getId());
+ AccountState createdAccount = createAccount(accountUpdater);
+ return toTestAccount(createdAccount);
+ }
+
+ private AccountState createAccount(AccountsUpdate.AccountUpdater accountUpdater)
+ throws OrmException, IOException, ConfigInvalidException {
+ Account.Id accountId = new Account.Id(seq.nextAccountId());
+ return accountsUpdate.insert("Create Test Account", accountId, accountUpdater);
+ }
+
+ private static void fillBuilder(
+ InternalAccountUpdate.Builder builder,
+ TestAccountCreation accountCreation,
+ Account.Id accountId) {
+ accountCreation.fullname().ifPresent(builder::setFullName);
+ accountCreation.preferredEmail().ifPresent(e -> setPreferredEmail(builder, accountId, e));
+ String httpPassword = accountCreation.httpPassword().orElse(null);
+ accountCreation.username().ifPresent(u -> setUsername(builder, accountId, u, httpPassword));
+ accountCreation.status().ifPresent(builder::setStatus);
+ accountCreation.active().ifPresent(builder::setActive);
+ }
+
+ private static TestAccount toTestAccount(AccountState accountState) {
+ Account createdAccount = accountState.getAccount();
+ return TestAccount.builder()
+ .accountId(createdAccount.getId())
+ .preferredEmail(Optional.ofNullable(createdAccount.getPreferredEmail()))
+ .fullname(Optional.ofNullable(createdAccount.getFullName()))
+ .username(accountState.getUserName())
+ .active(accountState.getAccount().isActive())
+ .build();
+ }
+
+ private static InternalAccountUpdate.Builder setPreferredEmail(
+ InternalAccountUpdate.Builder builder, Account.Id accountId, String preferredEmail) {
+ return builder
+ .setPreferredEmail(preferredEmail)
+ .addExternalId(ExternalId.createEmail(accountId, preferredEmail));
+ }
+
+ private static InternalAccountUpdate.Builder setUsername(
+ InternalAccountUpdate.Builder builder,
+ Account.Id accountId,
+ String username,
+ String httpPassword) {
+ return builder.addExternalId(ExternalId.createUsername(username, accountId, httpPassword));
+ }
+
+ private class MoreAccountOperationsImpl implements MoreAccountOperations {
+ private final Account.Id accountId;
+
+ MoreAccountOperationsImpl(Account.Id accountId) {
+ this.accountId = accountId;
+ }
+
+ @Override
+ public boolean exists() throws Exception {
+ return accounts.get(accountId).isPresent();
+ }
+
+ @Override
+ public TestAccount get() throws Exception {
+ AccountState account =
+ accounts
+ .get(accountId)
+ .orElseThrow(
+ () -> new IllegalStateException("Tried to get non-existing test account"));
+ return toTestAccount(account);
+ }
+
+ @Override
+ public TestAccountUpdate.Builder forUpdate() {
+ return TestAccountUpdate.builder(this::updateAccount);
+ }
+
+ private TestAccount updateAccount(TestAccountUpdate accountUpdate)
+ throws OrmException, IOException, ConfigInvalidException {
+ AccountsUpdate.AccountUpdater accountUpdater =
+ (account, updateBuilder) -> fillBuilder(updateBuilder, accountUpdate, accountId);
+ Optional<AccountState> updatedAccount = updateAccount(accountUpdater);
+ checkState(updatedAccount.isPresent(), "Tried to update non-existing test account");
+ return toTestAccount(updatedAccount.get());
+ }
+
+ private Optional<AccountState> updateAccount(AccountsUpdate.AccountUpdater accountUpdater)
+ throws OrmException, IOException, ConfigInvalidException {
+ return accountsUpdate.update("Update Test Account", accountId, accountUpdater);
+ }
+
+ private void fillBuilder(
+ InternalAccountUpdate.Builder builder,
+ TestAccountUpdate accountUpdate,
+ Account.Id accountId) {
+ accountUpdate.fullname().ifPresent(builder::setFullName);
+ accountUpdate.preferredEmail().ifPresent(e -> setPreferredEmail(builder, accountId, e));
+ String httpPassword = accountUpdate.httpPassword().orElse(null);
+ accountUpdate.username().ifPresent(u -> setUsername(builder, accountId, u, httpPassword));
+ accountUpdate.status().ifPresent(builder::setStatus);
+ accountUpdate.active().ifPresent(builder::setActive);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccount.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccount.java
new file mode 100644
index 0000000..e7ffeec
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccount.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.account;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.reviewdb.client.Account;
+import java.util.Optional;
+
+@AutoValue
+public abstract class TestAccount {
+ public abstract Account.Id accountId();
+
+ public abstract Optional<String> fullname();
+
+ public abstract Optional<String> preferredEmail();
+
+ public abstract Optional<String> username();
+
+ public abstract boolean active();
+
+ static Builder builder() {
+ return new AutoValue_TestAccount.Builder();
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder accountId(Account.Id accountId);
+
+ abstract Builder fullname(Optional<String> fullname);
+
+ abstract Builder preferredEmail(Optional<String> fullname);
+
+ abstract Builder username(Optional<String> username);
+
+ abstract Builder active(boolean active);
+
+ abstract TestAccount build();
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
new file mode 100644
index 0000000..a82d180
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountCreation.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.account;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import java.util.Optional;
+
+@AutoValue
+public abstract class TestAccountCreation {
+ public abstract Optional<String> fullname();
+
+ public abstract Optional<String> httpPassword();
+
+ public abstract Optional<String> preferredEmail();
+
+ public abstract Optional<String> username();
+
+ public abstract Optional<String> status();
+
+ public abstract Optional<Boolean> active();
+
+ abstract ThrowingFunction<TestAccountCreation, TestAccount> accountCreator();
+
+ public static Builder builder(ThrowingFunction<TestAccountCreation, TestAccount> accountCreator) {
+ return new AutoValue_TestAccountCreation.Builder()
+ .accountCreator(accountCreator)
+ .httpPassword("http-pass");
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder fullname(String fullname);
+
+ public Builder clearFullname() {
+ return fullname("");
+ }
+
+ public abstract Builder httpPassword(String httpPassword);
+
+ public Builder clearHttpPassword() {
+ return httpPassword("");
+ }
+
+ public abstract Builder preferredEmail(String preferredEmail);
+
+ public Builder clearPreferredEmail() {
+ return preferredEmail("");
+ }
+
+ public abstract Builder username(String username);
+
+ public Builder clearUsername() {
+ return username("");
+ }
+
+ public abstract Builder status(String status);
+
+ public Builder clearStatus() {
+ return status("");
+ }
+
+ abstract Builder active(boolean active);
+
+ public Builder active() {
+ return active(true);
+ }
+
+ public Builder inactive() {
+ return active(false);
+ }
+
+ abstract Builder accountCreator(
+ ThrowingFunction<TestAccountCreation, TestAccount> accountCreator);
+
+ abstract TestAccountCreation autoBuild();
+
+ public TestAccount create() throws Exception {
+ TestAccountCreation accountUpdate = autoBuild();
+ return accountUpdate.accountCreator().apply(accountUpdate);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestAccountUpdate.java b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountUpdate.java
new file mode 100644
index 0000000..517e4b5
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestAccountUpdate.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.account;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
+import java.util.Optional;
+
+@AutoValue
+public abstract class TestAccountUpdate {
+ public abstract Optional<String> fullname();
+
+ public abstract Optional<String> httpPassword();
+
+ public abstract Optional<String> preferredEmail();
+
+ public abstract Optional<String> username();
+
+ public abstract Optional<String> status();
+
+ public abstract Optional<Boolean> active();
+
+ abstract ThrowingFunction<TestAccountUpdate, TestAccount> accountUpdater();
+
+ public static Builder builder(ThrowingFunction<TestAccountUpdate, TestAccount> accountUpdater) {
+ return new AutoValue_TestAccountUpdate.Builder()
+ .accountUpdater(accountUpdater)
+ .httpPassword("http-pass");
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder fullname(String fullname);
+
+ public Builder clearFullname() {
+ return fullname("");
+ }
+
+ public abstract Builder httpPassword(String httpPassword);
+
+ public Builder clearHttpPassword() {
+ return httpPassword("");
+ }
+
+ public abstract Builder preferredEmail(String preferredEmail);
+
+ public Builder clearPreferredEmail() {
+ return preferredEmail("");
+ }
+
+ public abstract Builder username(String username);
+
+ public Builder clearUsername() {
+ return username("");
+ }
+
+ public abstract Builder status(String status);
+
+ public Builder clearStatus() {
+ return status("");
+ }
+
+ abstract Builder active(boolean active);
+
+ public Builder active() {
+ return active(true);
+ }
+
+ public Builder inactive() {
+ return active(false);
+ }
+
+ abstract Builder accountUpdater(
+ ThrowingFunction<TestAccountUpdate, TestAccount> accountUpdater);
+
+ abstract TestAccountUpdate autoBuild();
+
+ public TestAccount update() throws Exception {
+ TestAccountUpdate accountUpdate = autoBuild();
+ return accountUpdate.accountUpdater().apply(accountUpdate);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java b/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
new file mode 100644
index 0000000..0cb5cf3
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/testsuite/account/TestSshKeys.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.testsuite.account;
+
+import static com.google.common.base.Preconditions.checkState;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import com.google.gerrit.acceptance.SshEnabled;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.KeyPair;
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.Map;
+
+@Singleton
+public class TestSshKeys {
+ private final Map<String, KeyPair> sshKeyPairs;
+
+ private final VersionedAuthorizedKeys.Accessor authorizedKeys;
+ private final SshKeyCache sshKeyCache;
+ private final boolean sshEnabled;
+
+ @Inject
+ TestSshKeys(
+ VersionedAuthorizedKeys.Accessor authorizedKeys,
+ SshKeyCache sshKeyCache,
+ @SshEnabled boolean sshEnabled) {
+ this.authorizedKeys = authorizedKeys;
+ this.sshKeyCache = sshKeyCache;
+ this.sshEnabled = sshEnabled;
+ this.sshKeyPairs = new HashMap<>();
+ }
+
+ // TODO(ekempin): Remove this method when com.google.gerrit.acceptance.TestAccount is gone
+ public KeyPair getKeyPair(com.google.gerrit.acceptance.TestAccount account) throws Exception {
+ checkState(sshEnabled, "Requested SSH key pair, but SSH is disabled");
+ checkState(
+ account.username != null,
+ "Requested SSH key pair for account %s, but username is not set",
+ account.id);
+
+ String username = account.username;
+ KeyPair keyPair = sshKeyPairs.get(username);
+ if (keyPair == null) {
+ keyPair = createKeyPair(account.id, username, account.email);
+ sshKeyPairs.put(username, keyPair);
+ }
+ return keyPair;
+ }
+
+ public KeyPair getKeyPair(TestAccount account) throws Exception {
+ checkState(sshEnabled, "Requested SSH key pair, but SSH is disabled");
+ checkState(
+ account.username().isPresent(),
+ "Requested SSH key pair for account %s, but username is not set",
+ account.accountId());
+
+ String username = account.username().get();
+ KeyPair keyPair = sshKeyPairs.get(username);
+ if (keyPair == null) {
+ keyPair = createKeyPair(account.accountId(), username, account.preferredEmail().orElse(null));
+ sshKeyPairs.put(username, keyPair);
+ }
+ return keyPair;
+ }
+
+ private KeyPair createKeyPair(Account.Id accountId, String username, @Nullable String email)
+ throws Exception {
+ KeyPair keyPair = genSshKey();
+ authorizedKeys.addKey(accountId, publicKey(keyPair, email));
+ sshKeyCache.evict(username);
+ return keyPair;
+ }
+
+ public static KeyPair genSshKey() throws JSchException {
+ JSch jsch = new JSch();
+ return KeyPair.genKeyPair(jsch, KeyPair.ECDSA, 256);
+ }
+
+ public static String publicKey(KeyPair sshKey, @Nullable String comment)
+ throws UnsupportedEncodingException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ sshKey.writePublicKey(out, comment);
+ return out.toString(US_ASCII.name()).trim();
+ }
+
+ public static byte[] privateKey(KeyPair keyPair) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ keyPair.writePrivateKey(out);
+ return out.toByteArray();
+ }
+}
diff --git a/java/com/google/gerrit/common/BUILD b/java/com/google/gerrit/common/BUILD
index 919f532..2565f0d 100644
--- a/java/com/google/gerrit/common/BUILD
+++ b/java/com/google/gerrit/common/BUILD
@@ -22,6 +22,8 @@
"//lib:guava",
"//lib:gwtorm_client",
"//lib:servlet-api-3_1",
+ "//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/jgit/org.eclipse.jgit:jgit",
"//lib/log:api",
],
@@ -46,6 +48,8 @@
"//lib:gwtjsonrpc",
"//lib:gwtorm",
"//lib:servlet-api-3_1",
+ "//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/jgit/org.eclipse.jgit:jgit",
"//lib/log:api",
],
diff --git a/java/com/google/gerrit/common/data/AccessSection.java b/java/com/google/gerrit/common/data/AccessSection.java
index 2f1e199..95f48b1 100644
--- a/java/com/google/gerrit/common/data/AccessSection.java
+++ b/java/com/google/gerrit/common/data/AccessSection.java
@@ -75,8 +75,15 @@
return null;
}
- public void addPermission(Permission p) {
- getPermissions().add(p);
+ public void addPermission(Permission permission) {
+ List<Permission> permissions = getPermissions();
+ for (Permission p : permissions) {
+ if (p.getName().equalsIgnoreCase(permission.getName())) {
+ throw new IllegalArgumentException();
+ }
+ }
+
+ permissions.add(permission);
}
public void remove(Permission permission) {
diff --git a/java/com/google/gerrit/common/data/GlobalCapability.java b/java/com/google/gerrit/common/data/GlobalCapability.java
index c15f7b9..e613d21 100644
--- a/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -114,6 +114,9 @@
/** Can view all pending tasks in the queue (not just the filtered set). */
public static final String VIEW_QUEUE = "viewQueue";
+ /** Can query permissions for any (project, user) pair */
+ public static final String VIEW_ACCESS = "viewAccess";
+
private static final List<String> NAMES_ALL;
private static final List<String> NAMES_LC;
private static final String[] RANGE_NAMES = {
@@ -143,6 +146,7 @@
NAMES_ALL.add(VIEW_CONNECTIONS);
NAMES_ALL.add(VIEW_PLUGINS);
NAMES_ALL.add(VIEW_QUEUE);
+ NAMES_ALL.add(VIEW_ACCESS);
NAMES_LC = new ArrayList<>(NAMES_ALL.size());
for (String name : NAMES_ALL) {
diff --git a/java/com/google/gerrit/common/data/Permission.java b/java/com/google/gerrit/common/data/Permission.java
index b82b931..4ee176b 100644
--- a/java/com/google/gerrit/common/data/Permission.java
+++ b/java/com/google/gerrit/common/data/Permission.java
@@ -144,14 +144,14 @@
return extractLabel(getName());
}
- public Boolean getExclusiveGroup() {
+ public boolean getExclusiveGroup() {
// Only permit exclusive group behavior on non OWNER permissions,
// otherwise an owner might lose access to a delegated subspace.
//
return exclusiveGroup && !OWNER.equals(getName());
}
- public void setExclusiveGroup(Boolean newExclusiveGroup) {
+ public void setExclusiveGroup(boolean newExclusiveGroup) {
exclusiveGroup = newExclusiveGroup;
}
diff --git a/java/com/google/gerrit/common/data/SubmitRecord.java b/java/com/google/gerrit/common/data/SubmitRecord.java
index 3cd9c43..ae8b2bb 100644
--- a/java/com/google/gerrit/common/data/SubmitRecord.java
+++ b/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -14,6 +14,7 @@
package com.google.gerrit.common.data;
+import com.google.common.annotations.GwtIncompatible;
import com.google.gerrit.reviewdb.client.Account;
import java.util.Collection;
import java.util.List;
@@ -60,7 +61,7 @@
public Status status;
public List<Label> labels;
- public List<SubmitRequirement> requirements;
+ @GwtIncompatible public List<SubmitRequirement> requirements;
public String errorMessage;
public static class Label {
@@ -131,6 +132,7 @@
}
}
+ @GwtIncompatible
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
@@ -158,6 +160,7 @@
return sb.toString();
}
+ @GwtIncompatible
@Override
public boolean equals(Object o) {
if (o instanceof SubmitRecord) {
@@ -170,6 +173,7 @@
return false;
}
+ @GwtIncompatible
@Override
public int hashCode() {
return Objects.hash(status, labels, errorMessage, requirements);
diff --git a/java/com/google/gerrit/common/data/SubmitRequirement.java b/java/com/google/gerrit/common/data/SubmitRequirement.java
index a75f3c0..0a8d5ac0 100644
--- a/java/com/google/gerrit/common/data/SubmitRequirement.java
+++ b/java/com/google/gerrit/common/data/SubmitRequirement.java
@@ -14,67 +14,65 @@
package com.google.gerrit.common.data;
-import static java.util.Objects.requireNonNull;
-
-import com.google.gerrit.common.Nullable;
-import java.util.Objects;
-import java.util.Optional;
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.GwtIncompatible;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
/** Describes a requirement to submit a change. */
-public final class SubmitRequirement {
- private final String shortReason;
- private final String fullReason;
- @Nullable private final String label;
+@GwtIncompatible
+@AutoValue
+@AutoValue.CopyAnnotations
+public abstract class SubmitRequirement {
+ private static final CharMatcher TYPE_MATCHER =
+ CharMatcher.inRange('a', 'z')
+ .or(CharMatcher.inRange('A', 'Z'))
+ .or(CharMatcher.inRange('0', '9'))
+ .or(CharMatcher.anyOf("-_"));
- public SubmitRequirement(String shortReason, String fullReason, @Nullable String label) {
- this.shortReason = requireNonNull(shortReason);
- this.fullReason = requireNonNull(fullReason);
- this.label = label;
- }
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setType(String value);
- public String shortReason() {
- return shortReason;
- }
+ public abstract Builder setFallbackText(String value);
- public String fullReason() {
- return fullReason;
- }
-
- public Optional<String> label() {
- return Optional.ofNullable(label);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
+ public Builder setData(Map<String, String> value) {
+ return setData(ImmutableMap.copyOf(value));
}
- if (o instanceof SubmitRequirement) {
- SubmitRequirement that = (SubmitRequirement) o;
- return Objects.equals(shortReason, that.shortReason)
- && Objects.equals(fullReason, that.fullReason)
- && Objects.equals(label, that.label);
+
+ public Builder addCustomValue(String key, String value) {
+ dataBuilder().put(key, value);
+ return this;
}
- return false;
+
+ public SubmitRequirement build() {
+ SubmitRequirement requirement = autoBuild();
+ Preconditions.checkState(
+ validateType(requirement.type()),
+ "SubmitRequirement's type contains non alphanumerical symbols.");
+ return requirement;
+ }
+
+ abstract Builder setData(ImmutableMap<String, String> value);
+
+ abstract ImmutableMap.Builder<String, String> dataBuilder();
+
+ abstract SubmitRequirement autoBuild();
}
- @Override
- public int hashCode() {
- return Objects.hash(shortReason, fullReason, label);
+ public abstract String fallbackText();
+
+ public abstract String type();
+
+ public abstract ImmutableMap<String, String> data();
+
+ public static Builder builder() {
+ return new AutoValue_SubmitRequirement.Builder();
}
- @Override
- public String toString() {
- return "SubmitRequirement{"
- + "shortReason='"
- + shortReason
- + '\''
- + ", fullReason='"
- + fullReason
- + '\''
- + ", label='"
- + label
- + '\''
- + '}';
+ private static boolean validateType(String type) {
+ return TYPE_MATCHER.matchesAllOf(type);
}
}
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 3ad31ec..d4d33c7 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -189,21 +189,24 @@
}
private String toDocument(V v) throws IOException {
- XContentBuilder builder = jsonBuilder().startObject();
- for (Values<V> values : schema.buildFields(v)) {
- String name = values.getField().getName();
- if (values.getField().isRepeatable()) {
- builder.field(
- name,
- Streams.stream(values.getValues()).filter(e -> shouldAddElement(e)).collect(toList()));
- } else {
- Object element = Iterables.getOnlyElement(values.getValues(), "");
- if (shouldAddElement(element)) {
- builder.field(name, element);
+ try (XContentBuilder builder = jsonBuilder().startObject()) {
+ for (Values<V> values : schema.buildFields(v)) {
+ String name = values.getField().getName();
+ if (values.getField().isRepeatable()) {
+ builder.field(
+ name,
+ Streams.stream(values.getValues())
+ .filter(e -> shouldAddElement(e))
+ .collect(toList()));
+ } else {
+ Object element = Iterables.getOnlyElement(values.getValues(), "");
+ if (shouldAddElement(element)) {
+ builder.field(name, element);
+ }
}
}
+ return builder.endObject().string();
}
- return builder.endObject().string();
}
protected abstract V fromDocument(JsonObject doc, Set<String> fields);
diff --git a/java/com/google/gerrit/elasticsearch/BUILD b/java/com/google/gerrit/elasticsearch/BUILD
index 0aa7a39..f5ada85 100644
--- a/java/com/google/gerrit/elasticsearch/BUILD
+++ b/java/com/google/gerrit/elasticsearch/BUILD
@@ -17,11 +17,11 @@
"//lib/commons:codec",
"//lib/commons:lang",
"//lib/elasticsearch",
- "//lib/elasticsearch:jest",
- "//lib/elasticsearch:jest-common",
"//lib/elasticsearch:joda-time",
"//lib/guice",
"//lib/guice:guice-assistedinject",
+ "//lib/jest",
+ "//lib/jest:jest-common",
"//lib/jgit/org.eclipse.jgit:jgit",
"//lib/log:api",
"//lib/lucene:lucene-analyzers-common",
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index fe2554d..0a06c31 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -69,6 +69,7 @@
import java.io.IOException;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.lib.Config;
@@ -221,9 +222,24 @@
// Changed lines.
int added = addedElement.getAsInt();
int deleted = deletedElement.getAsInt();
- if (added != 0 && deleted != 0) {
- cd.setChangedLines(added, deleted);
+ cd.setChangedLines(added, deleted);
+ }
+
+ // Star.
+ JsonElement starredElement = source.get(ChangeField.STAR.getName());
+ if (starredElement != null) {
+ ListMultimap<Account.Id, String> stars = MultimapBuilder.hashKeys().arrayListValues().build();
+ JsonArray starBy = starredElement.getAsJsonArray();
+ if (starBy.size() > 0) {
+ for (int i = 0; i < starBy.size(); i++) {
+ String[] indexableFields = starBy.get(i).getAsString().split(":");
+ Optional<Account.Id> id = Account.Id.tryParse(indexableFields[0]);
+ if (id.isPresent()) {
+ stars.put(id.get(), indexableFields[1]);
+ }
+ }
}
+ cd.setStars(stars);
}
// Mergeable.
diff --git a/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java b/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
index 5a9c6ac..76fdfea 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -14,28 +14,15 @@
package com.google.gerrit.elasticsearch;
-import static com.google.common.base.Preconditions.checkArgument;
-
-import com.google.gerrit.index.IndexConfig;
-import com.google.gerrit.index.Schema;
import com.google.gerrit.index.project.ProjectIndex;
-import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.index.OnlineUpgrader;
-import com.google.gerrit.server.index.SingleVersionModule;
+import com.google.gerrit.server.index.AbstractIndexModule;
import com.google.gerrit.server.index.VersionManager;
import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.group.GroupIndex;
-import com.google.inject.AbstractModule;
-import com.google.inject.Provides;
-import com.google.inject.Singleton;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
import java.util.Map;
-import org.eclipse.jgit.lib.Config;
-public class ElasticIndexModule extends AbstractModule {
+public class ElasticIndexModule extends AbstractIndexModule {
public static ElasticIndexModule singleVersionWithExplicitVersions(
Map<String, Integer> versions, int threads, boolean slave) {
return new ElasticIndexModule(versions, threads, false, slave);
@@ -49,74 +36,33 @@
return new ElasticIndexModule(null, 0, false, slave);
}
- private final Map<String, Integer> singleVersions;
- private final int threads;
- private final boolean onlineUpgrade;
- private final boolean slave;
-
private ElasticIndexModule(
Map<String, Integer> singleVersions, int threads, boolean onlineUpgrade, boolean slave) {
- if (singleVersions != null) {
- checkArgument(!onlineUpgrade, "online upgrade is incompatible with single version map");
- }
- this.singleVersions = singleVersions;
- this.threads = threads;
- this.onlineUpgrade = onlineUpgrade;
- this.slave = slave;
+ super(singleVersions, threads, onlineUpgrade, slave);
}
@Override
- protected void configure() {
- if (slave) {
- bind(AccountIndex.Factory.class).toInstance(ElasticIndexModule::createDummyIndexFactory);
- bind(ChangeIndex.Factory.class).toInstance(ElasticIndexModule::createDummyIndexFactory);
- bind(ProjectIndex.Factory.class).toInstance(ElasticIndexModule::createDummyIndexFactory);
- } else {
- install(
- new FactoryModuleBuilder()
- .implement(AccountIndex.class, ElasticAccountIndex.class)
- .build(AccountIndex.Factory.class));
- install(
- new FactoryModuleBuilder()
- .implement(ChangeIndex.class, ElasticChangeIndex.class)
- .build(ChangeIndex.Factory.class));
- install(
- new FactoryModuleBuilder()
- .implement(ProjectIndex.class, ElasticProjectIndex.class)
- .build(ProjectIndex.Factory.class));
- }
- install(
- new FactoryModuleBuilder()
- .implement(GroupIndex.class, ElasticGroupIndex.class)
- .build(GroupIndex.Factory.class));
-
- install(new IndexModule(threads, slave));
- if (singleVersions == null) {
- install(new MultiVersionModule());
- } else {
- install(new SingleVersionModule(singleVersions));
- }
+ protected Class<? extends AccountIndex> getAccountIndex() {
+ return ElasticAccountIndex.class;
}
- @SuppressWarnings("unused")
- private static <T> T createDummyIndexFactory(Schema<?> schema) {
- throw new UnsupportedOperationException();
+ @Override
+ protected Class<? extends ChangeIndex> getChangeIndex() {
+ return ElasticChangeIndex.class;
}
- @Provides
- @Singleton
- IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
- return IndexConfig.fromConfig(cfg).separateChangeSubIndexes(true).build();
+ @Override
+ protected Class<? extends GroupIndex> getGroupIndex() {
+ return ElasticGroupIndex.class;
}
- private class MultiVersionModule extends LifecycleModule {
- @Override
- public void configure() {
- bind(VersionManager.class).to(ElasticVersionManager.class);
- listener().to(ElasticVersionManager.class);
- if (onlineUpgrade) {
- listener().to(OnlineUpgrader.class);
- }
- }
+ @Override
+ protected Class<? extends ProjectIndex> getProjectIndex() {
+ return ElasticProjectIndex.class;
+ }
+
+ @Override
+ protected Class<? extends VersionManager> getVersionManager() {
+ return ElasticVersionManager.class;
}
}
diff --git a/java/com/google/gerrit/extensions/api/access/GerritPermission.java b/java/com/google/gerrit/extensions/api/access/GerritPermission.java
new file mode 100644
index 0000000..133de31
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/access/GerritPermission.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.access;
+
+import java.util.Locale;
+
+/** Gerrit permission for hosts, projects, refs, changes, labels and plugins. */
+public interface GerritPermission {
+ /** @return readable identifier of this permission for exception message. */
+ String describeForException();
+
+ static String describeEnumValue(Enum<?> value) {
+ return value.name().toLowerCase(Locale.US).replace('_', ' ');
+ }
+}
diff --git a/java/com/google/gerrit/extensions/api/access/GlobalOrPluginPermission.java b/java/com/google/gerrit/extensions/api/access/GlobalOrPluginPermission.java
index deae084..95b887d 100644
--- a/java/com/google/gerrit/extensions/api/access/GlobalOrPluginPermission.java
+++ b/java/com/google/gerrit/extensions/api/access/GlobalOrPluginPermission.java
@@ -17,10 +17,4 @@
/**
* A {@link com.google.gerrit.server.permissions.GlobalPermission} or a {@link PluginPermission}.
*/
-public interface GlobalOrPluginPermission {
- /** @return name used in {@code project.config} permissions. */
- public String permissionName();
-
- /** @return readable identifier of this permission for exception message. */
- public String describeForException();
-}
+public interface GlobalOrPluginPermission extends GerritPermission {}
diff --git a/java/com/google/gerrit/extensions/api/access/PluginPermission.java b/java/com/google/gerrit/extensions/api/access/PluginPermission.java
index 7a467b8..449135d 100644
--- a/java/com/google/gerrit/extensions/api/access/PluginPermission.java
+++ b/java/com/google/gerrit/extensions/api/access/PluginPermission.java
@@ -47,11 +47,6 @@
}
@Override
- public String permissionName() {
- return pluginName + '-' + capability;
- }
-
- @Override
public String describeForException() {
return capability + " for plugin " + pluginName;
}
diff --git a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index 912ad64..0c3a11f 100644
--- a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -78,6 +78,10 @@
void deleteEmail(String email) throws RestApiException;
+ EmailApi createEmail(EmailInput emailInput) throws RestApiException;
+
+ EmailApi email(String email) throws RestApiException;
+
void setStatus(String status) throws RestApiException;
List<SshKeyInfo> listSshKeys() throws RestApiException;
@@ -220,6 +224,16 @@
}
@Override
+ public EmailApi createEmail(EmailInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public EmailApi email(String email) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public void setStatus(String status) throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/api/accounts/EmailApi.java b/java/com/google/gerrit/extensions/api/accounts/EmailApi.java
new file mode 100644
index 0000000..da038c3
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/accounts/EmailApi.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.accounts;
+
+import com.google.gerrit.extensions.common.EmailInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface EmailApi {
+ EmailInfo get() throws RestApiException;
+
+ void delete() throws RestApiException;
+
+ void setPreferred() throws RestApiException;
+
+ /**
+ * A default implementation which allows source compatibility when adding new methods to the
+ * interface.
+ */
+ class NotImplemented implements EmailApi {
+ @Override
+ public EmailInfo get() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public void delete() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public void setPreferred() throws RestApiException {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index a13fb75..b140064 100644
--- a/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -36,15 +36,6 @@
public Map<String, List<RobotCommentInput>> robotComments;
/**
- * If true require all labels to be within the user's permitted ranges based on access controls,
- * attempting to use a label not granted to the user will fail the entire modify operation early.
- * If false the operation will execute anyway, but the proposed labels given by the user will be
- * modified to be the "best" value allowed by the access controls, or ignored if the label does
- * not exist.
- */
- public boolean strictLabels = true;
-
- /**
* How to process draft comments already in the database that were not also described in this
* input request.
*
@@ -66,8 +57,6 @@
* on behalf of this named user instead of the caller. Caller must have the labelAs-$NAME
* permission granted for each label that appears in {@link #labels}. This is in addition to the
* named user also needing to have permission to use the labels.
- *
- * <p>{@link #strictLabels} impacts how labels is processed for the named user, not the caller.
*/
public String onBehalfOf;
diff --git a/java/com/google/gerrit/extensions/api/config/ConfigUpdateEntryInfo.java b/java/com/google/gerrit/extensions/api/config/ConfigUpdateEntryInfo.java
new file mode 100644
index 0000000..4ebf7b2
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/config/ConfigUpdateEntryInfo.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.config;
+
+public class ConfigUpdateEntryInfo {
+ public String configKey;
+ public String oldValue;
+ public String newValue;
+}
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index f802049..c95dcc3 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -70,4 +70,5 @@
public List<ProblemInfo> problems;
public List<PluginDefinedInfo> plugins;
public Collection<TrackingIdInfo> trackingIds;
+ public Collection<SubmitRequirementInfo> requirements;
}
diff --git a/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java b/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java
new file mode 100644
index 0000000..a940403
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/SubmitRequirementInfo.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.Map;
+import java.util.Objects;
+
+public class SubmitRequirementInfo {
+ public final String status;
+ public final String fallbackText;
+ public final String type;
+ public final Map<String, String> data;
+
+ public SubmitRequirementInfo(
+ String status, String fallbackText, String type, Map<String, String> data) {
+ this.status = status;
+ this.fallbackText = fallbackText;
+ this.type = type;
+ this.data = data;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SubmitRequirementInfo)) {
+ return false;
+ }
+ SubmitRequirementInfo that = (SubmitRequirementInfo) o;
+ return Objects.equals(status, that.status)
+ && Objects.equals(fallbackText, that.fallbackText)
+ && Objects.equals(type, that.type)
+ && Objects.equals(data, that.data);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(status, fallbackText, type, data);
+ }
+}
diff --git a/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java b/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
index 48f7f45..9030a1c 100644
--- a/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/ContentEntrySubject.java
@@ -17,6 +17,7 @@
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.IterableSubject;
import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
import com.google.common.truth.Truth;
@@ -68,4 +69,16 @@
ContentEntry contentEntry = actual();
return ListSubject.assertThat(contentEntry.b, Truth::assertThat).named("lines of 'b'");
}
+
+ public IterableSubject intralineEditsOfA() {
+ isNotNull();
+ ContentEntry contentEntry = actual();
+ return Truth.assertThat(contentEntry.editA).named("intraline edits of 'a'");
+ }
+
+ public IterableSubject intralineEditsOfB() {
+ isNotNull();
+ ContentEntry contentEntry = actual();
+ return Truth.assertThat(contentEntry.editB).named("intraline edits of 'b'");
+ }
}
diff --git a/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java b/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java
index 1b95778d..d368ed4 100644
--- a/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java
+++ b/java/com/google/gerrit/extensions/config/ExternalIncludedIn.java
@@ -23,8 +23,8 @@
/**
* Returns additional entries for IncludedInInfo as multimap where the key is the row title and
- * the the values are a list of systems that include the given commit (e.g. names of servers on
- * which this commit is deployed).
+ * the values are a list of systems that include the given commit (e.g. names of servers on which
+ * this commit is deployed).
*
* <p>The tags and branches in which the commit is included are provided so that a RevWalk can be
* avoided when a system runs a certain tag or branch.
diff --git a/java/com/google/gerrit/git/testing/BUILD b/java/com/google/gerrit/git/testing/BUILD
new file mode 100644
index 0000000..0b83560
--- /dev/null
+++ b/java/com/google/gerrit/git/testing/BUILD
@@ -0,0 +1,14 @@
+package(default_testonly = 1)
+
+java_library(
+ name = "testing",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//java/com/google/gerrit/common:annotations",
+ "//lib:guava",
+ "//lib:truth",
+ "//lib:truth-java8-extension",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
diff --git a/java/com/google/gerrit/git/testing/PushResultSubject.java b/java/com/google/gerrit/git/testing/PushResultSubject.java
new file mode 100644
index 0000000..c5163d1
--- /dev/null
+++ b/java/com/google/gerrit/git/testing/PushResultSubject.java
@@ -0,0 +1,172 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.git.testing;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.truth.Truth.assertAbout;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
+import com.google.common.truth.Truth;
+import com.google.common.truth.Truth8;
+import com.google.gerrit.common.Nullable;
+import java.util.Arrays;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+
+public class PushResultSubject extends Subject<PushResultSubject, PushResult> {
+ public static PushResultSubject assertThat(PushResult actual) {
+ return assertAbout(PushResultSubject::new).that(actual);
+ }
+
+ private PushResultSubject(FailureMetadata metadata, PushResult actual) {
+ super(metadata, actual);
+ }
+
+ public void hasNoMessages() {
+ Truth.assertWithMessage("expected no messages")
+ .that(Strings.nullToEmpty(trimMessages()))
+ .isEqualTo("");
+ }
+
+ public void hasMessages(String... expectedLines) {
+ checkArgument(expectedLines.length > 0, "use hasNoMessages()");
+ isNotNull();
+ Truth.assertThat(trimMessages()).isEqualTo(Arrays.stream(expectedLines).collect(joining("\n")));
+ }
+
+ private String trimMessages() {
+ return trimMessages(actual().getMessages());
+ }
+
+ @VisibleForTesting
+ @Nullable
+ static String trimMessages(@Nullable String msg) {
+ if (msg == null) {
+ return null;
+ }
+ int idx = msg.indexOf("Processing changes:");
+ if (idx >= 0) {
+ msg = msg.substring(0, idx);
+ }
+ return msg.trim();
+ }
+
+ public void hasProcessed(ImmutableMap<String, Integer> expected) {
+ ImmutableMap<String, Integer> actual;
+ String messages = actual().getMessages();
+ try {
+ actual = parseProcessed(messages);
+ } catch (RuntimeException e) {
+ Truth.assert_()
+ .fail(
+ "failed to parse \"Processing changes\" line from messages: %s\n%s",
+ messages, Throwables.getStackTraceAsString(e));
+ return;
+ }
+ Truth.assertThat(actual)
+ .named("processed commands")
+ .containsExactlyEntriesIn(expected)
+ .inOrder();
+ }
+
+ @VisibleForTesting
+ static ImmutableMap<String, Integer> parseProcessed(@Nullable String messages) {
+ if (messages == null) {
+ return ImmutableMap.of();
+ }
+ String toSplit = messages.trim();
+ String prefix = "Processing changes: ";
+ int idx = toSplit.lastIndexOf(prefix);
+ if (idx < 0) {
+ return ImmutableMap.of();
+ }
+ toSplit = toSplit.substring(idx + prefix.length());
+ if (toSplit.equals("done")) {
+ return ImmutableMap.of();
+ }
+ String done = ", done";
+ if (toSplit.endsWith(done)) {
+ toSplit = toSplit.substring(0, toSplit.length() - done.length());
+ }
+ return ImmutableMap.copyOf(
+ Maps.transformValues(
+ Splitter.on(',').trimResults().withKeyValueSeparator(':').split(toSplit),
+ // trimResults() doesn't trim values in the map.
+ v -> Integer.parseInt(v.trim())));
+ }
+
+ public RemoteRefUpdateSubject ref(String refName) {
+ return assertAbout(
+ (FailureMetadata m, RemoteRefUpdate a) -> new RemoteRefUpdateSubject(refName, m, a))
+ .that(actual().getRemoteUpdate(refName));
+ }
+
+ public RemoteRefUpdateSubject onlyRef(String refName) {
+ Truth8.assertThat(actual().getRemoteUpdates().stream().map(RemoteRefUpdate::getRemoteName))
+ .named("set of refs")
+ .containsExactly(refName);
+ return ref(refName);
+ }
+
+ public static class RemoteRefUpdateSubject
+ extends Subject<RemoteRefUpdateSubject, RemoteRefUpdate> {
+ private final String refName;
+
+ private RemoteRefUpdateSubject(
+ String refName, FailureMetadata metadata, RemoteRefUpdate actual) {
+ super(metadata, actual);
+ this.refName = refName;
+ named("ref update for %s", refName).isNotNull();
+ }
+
+ public void hasStatus(RemoteRefUpdate.Status status) {
+ RemoteRefUpdate u = actual();
+ Truth.assertThat(u.getStatus())
+ .named(
+ "status of ref update for %s%s",
+ refName, u.getMessage() != null ? ": " + u.getMessage() : "")
+ .isEqualTo(status);
+ }
+
+ public void hasNoMessage() {
+ Truth.assertThat(actual().getMessage())
+ .named("message of ref update for %s", refName)
+ .isNull();
+ }
+
+ public void hasMessage(String expected) {
+ Truth.assertThat(actual().getMessage())
+ .named("message of ref update for %s", refName)
+ .isEqualTo(expected);
+ }
+
+ public void isOk() {
+ hasStatus(RemoteRefUpdate.Status.OK);
+ }
+
+ public void isRejected(String expectedMessage) {
+ hasStatus(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
+ hasMessage(expectedMessage);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
index 7b97561..a636a8b 100644
--- a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -20,7 +20,9 @@
import com.google.common.io.BaseEncoding;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.server.GerritPersonIdent;
@@ -32,6 +34,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
+import java.util.Optional;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -60,19 +63,20 @@
@Override
public Response<?> apply(GpgKey rsrc, Input input)
- throws ResourceConflictException, PGPException, OrmException, IOException,
- ConfigInvalidException {
+ throws RestApiException, PGPException, OrmException, IOException, ConfigInvalidException {
PGPPublicKey key = rsrc.getKeyRing().getPublicKey();
- ExternalId extId =
- externalIds.get(
- ExternalId.Key.create(
- SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint())));
+ String fingerprint = BaseEncoding.base16().encode(key.getFingerprint());
+ Optional<ExternalId> extId = externalIds.get(ExternalId.Key.create(SCHEME_GPGKEY, fingerprint));
+ if (!extId.isPresent()) {
+ throw new ResourceNotFoundException(fingerprint);
+ }
+
accountsUpdateProvider
.get()
.update(
"Delete GPG Key via API",
rsrc.getUser().getAccountId(),
- u -> u.deleteExternalId(extId));
+ u -> u.deleteExternalId(extId.get()));
try (PublicKeyStore store = storeProvider.get()) {
store.remove(rsrc.getKeyRing().getPublicKey().getFingerprint());
diff --git a/java/com/google/gerrit/httpd/AllRequestFilter.java b/java/com/google/gerrit/httpd/AllRequestFilter.java
index e6918f70..b8b0bc8 100644
--- a/java/com/google/gerrit/httpd/AllRequestFilter.java
+++ b/java/com/google/gerrit/httpd/AllRequestFilter.java
@@ -106,7 +106,7 @@
throws IOException, ServletException {
while (itr.hasNext()) {
AllRequestFilter filter = itr.next();
- // To avoid {@code synchronized} on the the whole filtering (and
+ // To avoid {@code synchronized} on the whole filtering (and
// thereby killing concurrency), we start the below disjunction
// with an unsynchronized check for containment. This
// unsynchronized check is always correct if no filters got
diff --git a/java/com/google/gerrit/httpd/BUILD b/java/com/google/gerrit/httpd/BUILD
index d7cbdb8..b62c887 100644
--- a/java/com/google/gerrit/httpd/BUILD
+++ b/java/com/google/gerrit/httpd/BUILD
@@ -30,6 +30,7 @@
"//lib:servlet-api-3_1",
"//lib:soy",
"//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/commons:codec",
"//lib/guice",
"//lib/guice:guice-assistedinject",
diff --git a/java/com/google/gerrit/httpd/init/BUILD b/java/com/google/gerrit/httpd/init/BUILD
index 9624241..f240088 100644
--- a/java/com/google/gerrit/httpd/init/BUILD
+++ b/java/com/google/gerrit/httpd/init/BUILD
@@ -19,6 +19,7 @@
"//java/com/google/gerrit/server:module",
"//java/com/google/gerrit/server/api",
"//java/com/google/gerrit/server/cache/h2",
+ "//java/com/google/gerrit/server/cache/mem",
"//java/com/google/gerrit/server/git/receive",
"//java/com/google/gerrit/server/restapi",
"//java/com/google/gerrit/server/schema",
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index 7aa5c63..6cbb357 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -41,11 +41,13 @@
import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker;
import com.google.gerrit.pgm.util.LogFileCompressor;
import com.google.gerrit.server.LibModuleLoader;
+import com.google.gerrit.server.ModuleOverloader;
import com.google.gerrit.server.StartupChecks;
import com.google.gerrit.server.account.AccountDeactivator;
import com.google.gerrit.server.account.InternalAccountDirectory;
import com.google.gerrit.server.api.GerritApiModule;
-import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
+import com.google.gerrit.server.cache.h2.H2CacheModule;
+import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
import com.google.gerrit.server.change.ChangeCleanupRunner;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.AuthConfigModule;
@@ -57,13 +59,13 @@
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.GerritServerConfigModule;
import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.config.SysExecutorModule;
import com.google.gerrit.server.events.EventBroker;
import com.google.gerrit.server.events.StreamEventsApiListener;
import com.google.gerrit.server.git.GarbageCollectionModule;
import com.google.gerrit.server.git.GitRepositoryManagerModule;
import com.google.gerrit.server.git.SearchingChangeCacheImpl;
import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
import com.google.gerrit.server.index.IndexModule;
import com.google.gerrit.server.index.IndexModule.IndexType;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
@@ -328,7 +330,7 @@
modules.add(new JdbcAccountPatchReviewStore.Module(config));
modules.add(cfgInjector.getInstance(GitRepositoryManagerModule.class));
modules.add(new StreamEventsApiListener.Module());
- modules.add(new ReceiveCommitsExecutorModule());
+ modules.add(new SysExecutorModule());
modules.add(new DiffExecutorModule());
modules.add(new MimeUtil2Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
@@ -336,7 +338,8 @@
modules.add(new SearchingChangeCacheImpl.Module());
modules.add(new InternalAccountDirectory.Module());
modules.add(new DefaultPermissionBackendModule());
- modules.add(new DefaultCacheFactory.Module());
+ modules.add(new DefaultMemoryCacheModule());
+ modules.add(new H2CacheModule());
modules.add(cfgInjector.getInstance(MailReceiver.Module.class));
modules.add(new SmtpEmailSender.Module());
modules.add(new SignedTokenEmailTokenVerifier.Module());
@@ -384,9 +387,9 @@
modules.add(new GarbageCollectionModule());
modules.add(new ChangeCleanupRunner.Module());
modules.add(new AccountDeactivator.Module());
- modules.addAll(LibModuleLoader.loadModules(cfgInjector));
modules.add(new DefaultProjectNameLockManager.Module());
- return cfgInjector.createChildInjector(modules);
+ return cfgInjector.createChildInjector(
+ ModuleOverloader.override(modules, LibModuleLoader.loadModules(cfgInjector)));
}
private Module createIndexModule() {
@@ -413,9 +416,7 @@
false,
sysInjector.getInstance(DownloadConfig.class),
sysInjector.getInstance(LfsPluginAuthCommand.Module.class)));
- if (indexType == IndexType.LUCENE) {
- modules.add(new IndexCommandsModule());
- }
+ modules.add(new IndexCommandsModule(sysInjector));
return sysInjector.createChildInjector(modules);
}
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index 2cdca13..915e9ed 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -76,8 +76,8 @@
"/", "/c/*", "/p/*", "/q/*", "/x/*", "/admin/*", "/dashboard/*", "/settings/*");
// TODO(dborowitz): These fragments conflict with the REST API
// namespace, so they will need to use a different path.
- //"/groups/*",
- //"/projects/*");
+ // "/groups/*",
+ // "/projects/*");
//
/**
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 8352f57..dc2639b 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -102,6 +102,7 @@
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.audit.AuditService;
import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
+import com.google.gerrit.server.cache.PerThreadCache;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.LockFailureException;
import com.google.gerrit.server.permissions.GlobalPermission;
@@ -278,7 +279,7 @@
RestResource rsrc = TopLevelResource.INSTANCE;
ViewData viewData = null;
- try {
+ try (PerThreadCache ignored = PerThreadCache.create()) {
if (isCorsPreflight(req)) {
doCorsPreflight(req, res);
return;
diff --git a/java/com/google/gerrit/index/BUILD b/java/com/google/gerrit/index/BUILD
index 015eceb..f293b2d 100644
--- a/java/com/google/gerrit/index/BUILD
+++ b/java/com/google/gerrit/index/BUILD
@@ -42,6 +42,7 @@
"//lib:gwtorm",
"//lib/antlr:java_runtime",
"//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/jgit/org.eclipse.jgit:jgit",
"//lib/log:api",
],
diff --git a/java/com/google/gerrit/index/query/PostFilterPredicate.java b/java/com/google/gerrit/index/query/PostFilterPredicate.java
index 3e780bf..78b4c2b 100644
--- a/java/com/google/gerrit/index/query/PostFilterPredicate.java
+++ b/java/com/google/gerrit/index/query/PostFilterPredicate.java
@@ -18,4 +18,8 @@
* Matches all documents in the index, with additional filtering done in the subclass's {@code
* match} method.
*/
-public abstract class PostFilterPredicate<T> extends Predicate<T> implements Matchable<T> {}
+public abstract class PostFilterPredicate<T> extends OperatorPredicate<T> implements Matchable<T> {
+ public PostFilterPredicate(String operator, String value) {
+ super(operator, value);
+ }
+}
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index b7d232d..13dad0e 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -305,9 +305,9 @@
ClassLoader parent = ClassLoader.getSystemClassLoader();
if (!extapi.isEmpty()) {
- parent = new URLClassLoader(extapi.toArray(new URL[extapi.size()]), parent);
+ parent = URLClassLoader.newInstance(extapi.toArray(new URL[extapi.size()]), parent);
}
- return new URLClassLoader(jars.values().toArray(new URL[jars.size()]), parent);
+ return URLClassLoader.newInstance(jars.values().toArray(new URL[jars.size()]), parent);
}
private static void extractJar(ZipFile zf, ZipEntry ze, SortedMap<String, URL> jars)
@@ -718,7 +718,7 @@
dirs.add(u);
}
}
- return new URLClassLoader(
+ return URLClassLoader.newInstance(
dirs.toArray(new URL[dirs.size()]), ClassLoader.getSystemClassLoader().getParent());
}
diff --git a/java/com/google/gerrit/lucene/LuceneIndexModule.java b/java/com/google/gerrit/lucene/LuceneIndexModule.java
index 5c6cb27..121b96b 100644
--- a/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -14,30 +14,20 @@
package com.google.gerrit.lucene;
-import static com.google.common.base.Preconditions.checkArgument;
-
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.index.IndexConfig;
-import com.google.gerrit.index.Schema;
import com.google.gerrit.index.project.ProjectIndex;
-import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.index.IndexModule;
-import com.google.gerrit.server.index.OnlineUpgrader;
-import com.google.gerrit.server.index.SingleVersionModule;
+import com.google.gerrit.server.index.AbstractIndexModule;
import com.google.gerrit.server.index.VersionManager;
import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.group.GroupIndex;
-import com.google.inject.AbstractModule;
-import com.google.inject.Provides;
-import com.google.inject.Singleton;
-import com.google.inject.assistedinject.FactoryModuleBuilder;
import java.util.Map;
import org.apache.lucene.search.BooleanQuery;
import org.eclipse.jgit.lib.Config;
-public class LuceneIndexModule extends AbstractModule {
+public class LuceneIndexModule extends AbstractIndexModule {
public static LuceneIndexModule singleVersionAllLatest(int threads, boolean slave) {
return new LuceneIndexModule(ImmutableMap.of(), threads, false, slave);
}
@@ -59,76 +49,40 @@
return cfg.getBoolean("index", "lucene", "testInmemory", false);
}
- private final Map<String, Integer> singleVersions;
- private final int threads;
- private final boolean onlineUpgrade;
- private final boolean slave;
-
private LuceneIndexModule(
Map<String, Integer> singleVersions, int threads, boolean onlineUpgrade, boolean slave) {
- if (singleVersions != null) {
- checkArgument(!onlineUpgrade, "online upgrade is incompatible with single version map");
- }
- this.singleVersions = singleVersions;
- this.threads = threads;
- this.onlineUpgrade = onlineUpgrade;
- this.slave = slave;
+ super(singleVersions, threads, onlineUpgrade, slave);
}
@Override
- protected void configure() {
- if (slave) {
- bind(AccountIndex.Factory.class).toInstance(LuceneIndexModule::createDummyIndexFactory);
- bind(ChangeIndex.Factory.class).toInstance(LuceneIndexModule::createDummyIndexFactory);
- bind(ProjectIndex.Factory.class).toInstance(LuceneIndexModule::createDummyIndexFactory);
- } else {
- install(
- new FactoryModuleBuilder()
- .implement(AccountIndex.class, LuceneAccountIndex.class)
- .build(AccountIndex.Factory.class));
- install(
- new FactoryModuleBuilder()
- .implement(ChangeIndex.class, LuceneChangeIndex.class)
- .build(ChangeIndex.Factory.class));
- install(
- new FactoryModuleBuilder()
- .implement(ProjectIndex.class, LuceneProjectIndex.class)
- .build(ProjectIndex.Factory.class));
- }
- install(
- new FactoryModuleBuilder()
- .implement(GroupIndex.class, LuceneGroupIndex.class)
- .build(GroupIndex.Factory.class));
-
- install(new IndexModule(threads, slave));
- if (singleVersions == null) {
- install(new MultiVersionModule());
- } else {
- install(new SingleVersionModule(singleVersions));
- }
+ protected Class<? extends AccountIndex> getAccountIndex() {
+ return LuceneAccountIndex.class;
}
- @SuppressWarnings("unused")
- private static <T> T createDummyIndexFactory(Schema<?> schema) {
- throw new UnsupportedOperationException();
+ @Override
+ protected Class<? extends ChangeIndex> getChangeIndex() {
+ return LuceneChangeIndex.class;
}
- @Provides
- @Singleton
- IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
+ @Override
+ protected Class<? extends GroupIndex> getGroupIndex() {
+ return LuceneGroupIndex.class;
+ }
+
+ @Override
+ protected Class<? extends ProjectIndex> getProjectIndex() {
+ return LuceneProjectIndex.class;
+ }
+
+ @Override
+ protected Class<? extends VersionManager> getVersionManager() {
+ return LuceneVersionManager.class;
+ }
+
+ @Override
+ protected IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
BooleanQuery.setMaxClauseCount(
cfg.getInt("index", "maxTerms", BooleanQuery.getMaxClauseCount()));
- return IndexConfig.fromConfig(cfg).separateChangeSubIndexes(true).build();
- }
-
- private class MultiVersionModule extends LifecycleModule {
- @Override
- public void configure() {
- bind(VersionManager.class).to(LuceneVersionManager.class);
- listener().to(LuceneVersionManager.class);
- if (onlineUpgrade) {
- listener().to(OnlineUpgrader.class);
- }
- }
+ return super.getIndexConfig(cfg);
}
}
diff --git a/java/com/google/gerrit/pgm/BUILD b/java/com/google/gerrit/pgm/BUILD
index a255020..76421fc 100644
--- a/java/com/google/gerrit/pgm/BUILD
+++ b/java/com/google/gerrit/pgm/BUILD
@@ -37,6 +37,7 @@
"//java/com/google/gerrit/server:module",
"//java/com/google/gerrit/server/api",
"//java/com/google/gerrit/server/cache/h2",
+ "//java/com/google/gerrit/server/cache/mem",
"//java/com/google/gerrit/server/git/receive",
"//java/com/google/gerrit/server/restapi",
"//java/com/google/gerrit/server/schema",
@@ -49,6 +50,7 @@
"//lib:protobuf",
"//lib:servlet-api-3_1-without-neverlink",
"//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/guice",
"//lib/guice:guice-assistedinject",
"//lib/guice:guice-servlet",
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 1eb3e74..417b00d 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -49,12 +49,14 @@
import com.google.gerrit.pgm.util.RuntimeShutdown;
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.server.LibModuleLoader;
+import com.google.gerrit.server.ModuleOverloader;
import com.google.gerrit.server.StartupChecks;
import com.google.gerrit.server.account.AccountDeactivator;
import com.google.gerrit.server.account.InternalAccountDirectory;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
-import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
+import com.google.gerrit.server.cache.h2.H2CacheModule;
+import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
import com.google.gerrit.server.change.ChangeCleanupRunner;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.AuthConfigModule;
@@ -65,12 +67,12 @@
import com.google.gerrit.server.config.GerritInstanceNameModule;
import com.google.gerrit.server.config.GerritOptions;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SysExecutorModule;
import com.google.gerrit.server.events.EventBroker;
import com.google.gerrit.server.events.StreamEventsApiListener;
import com.google.gerrit.server.git.GarbageCollectionModule;
import com.google.gerrit.server.git.SearchingChangeCacheImpl;
import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
import com.google.gerrit.server.group.PeriodicGroupIndexer;
import com.google.gerrit.server.index.IndexModule;
import com.google.gerrit.server.index.IndexModule.IndexType;
@@ -412,7 +414,7 @@
inMemoryTest
? new InMemoryAccountPatchReviewStore.Module()
: new JdbcAccountPatchReviewStore.Module(config));
- modules.add(new ReceiveCommitsExecutorModule());
+ modules.add(new SysExecutorModule());
modules.add(new DiffExecutorModule());
modules.add(new MimeUtil2Module());
modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
@@ -422,7 +424,8 @@
modules.add(new SearchingChangeCacheImpl.Module(slave));
modules.add(new InternalAccountDirectory.Module());
modules.add(new DefaultPermissionBackendModule());
- modules.add(new DefaultCacheFactory.Module());
+ modules.add(new DefaultMemoryCacheModule());
+ modules.add(new H2CacheModule());
modules.add(cfgInjector.getInstance(MailReceiver.Module.class));
if (emailModule != null) {
modules.add(emailModule);
@@ -478,7 +481,6 @@
modules.add(new AccountDeactivator.Module());
modules.add(new ChangeCleanupRunner.Module());
}
- modules.addAll(LibModuleLoader.loadModules(cfgInjector));
if (migrateToNoteDb()) {
modules.add(new OnlineNoteDbMigrator.Module(trial));
}
@@ -487,7 +489,8 @@
}
modules.add(new LocalMergeSuperSetComputation.Module());
modules.add(new DefaultProjectNameLockManager.Module());
- return cfgInjector.createChildInjector(modules);
+ return cfgInjector.createChildInjector(
+ ModuleOverloader.override(modules, LibModuleLoader.loadModules(cfgInjector)));
}
private boolean migrateToNoteDb() {
@@ -544,8 +547,8 @@
slave,
sysInjector.getInstance(DownloadConfig.class),
sysInjector.getInstance(LfsPluginAuthCommand.Module.class)));
- if (!slave && indexType == IndexType.LUCENE) {
- modules.add(new IndexCommandsModule());
+ if (!slave) {
+ modules.add(new IndexCommandsModule(sysInjector));
}
return sysInjector.createChildInjector(modules);
}
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 6e41a07..d0aed46 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -145,7 +145,7 @@
if (sshKey != null) {
VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
- authorizedKeys.addKey(sshKey.getSshPublicKey());
+ authorizedKeys.addKey(sshKey.sshPublicKey());
authorizedKeys.save("Add SSH key for initial admin user\n");
}
@@ -165,8 +165,8 @@
private String readEmail(AccountSshKey sshKey) {
String defaultEmail = "admin@example.com";
- if (sshKey != null && sshKey.getComment() != null) {
- String c = sshKey.getComment().trim();
+ if (sshKey != null && sshKey.comment() != null) {
+ String c = sshKey.comment().trim();
if (EmailValidator.getInstance().isValid(c)) {
defaultEmail = c;
}
@@ -199,6 +199,6 @@
throw new IOException(String.format("Cannot add public SSH key: %s is not a file", keyFile));
}
String content = new String(Files.readAllBytes(p), UTF_8);
- return new AccountSshKey(new AccountSshKey.Id(id, 1), content);
+ return AccountSshKey.create(id, 1, content);
}
}
diff --git a/java/com/google/gerrit/pgm/init/InitIndex.java b/java/com/google/gerrit/pgm/init/InitIndex.java
index 93e0f5d7..ee6c440 100644
--- a/java/com/google/gerrit/pgm/init/InitIndex.java
+++ b/java/com/google/gerrit/pgm/init/InitIndex.java
@@ -70,6 +70,7 @@
"Transport protocol", "protocol", "http", Sets.newHashSet("http", "https"));
defaultServer.string("Hostname", "hostname", "localhost");
defaultServer.string("Port", "port", "9200");
+ index.string("Result window size", "maxLimit", "10000");
}
if ((site.isNew || isEmptySite()) && type == IndexType.LUCENE) {
diff --git a/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java b/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
index c1d142b..a7f9c5d 100644
--- a/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
+++ b/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
@@ -62,11 +62,10 @@
return pluginsInitSteps;
}
- @SuppressWarnings("resource")
private InitStep loadInitStep(Path jar) {
try {
URLClassLoader pluginLoader =
- new URLClassLoader(
+ URLClassLoader.newInstance(
new URL[] {jar.toUri().toURL()}, InitPluginStepsLoader.class.getClassLoader());
try (JarFile jarFile = new JarFile(jar.toFile())) {
Attributes jarFileAttributes = jarFile.getManifest().getMainAttributes();
diff --git a/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java b/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
index 757c9a4..a9c6cc8 100644
--- a/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
+++ b/java/com/google/gerrit/pgm/init/VersionedAuthorizedKeysOnInit.java
@@ -66,8 +66,8 @@
public AccountSshKey addKey(String pub) {
checkState(keys != null, "SSH keys not loaded yet");
int seq = keys.isEmpty() ? 1 : keys.size() + 1;
- AccountSshKey.Id keyId = new AccountSshKey.Id(accountId, seq);
- AccountSshKey key = new VersionedAuthorizedKeys.SimpleSshKeyCreator().create(keyId, pub);
+ AccountSshKey key =
+ new VersionedAuthorizedKeys.SimpleSshKeyCreator().create(accountId, seq, pub);
keys.add(Optional.of(key));
return key;
}
diff --git a/java/com/google/gerrit/pgm/util/BUILD b/java/com/google/gerrit/pgm/util/BUILD
index d6e44bd..91647fb 100644
--- a/java/com/google/gerrit/pgm/util/BUILD
+++ b/java/com/google/gerrit/pgm/util/BUILD
@@ -12,6 +12,7 @@
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server:module",
"//java/com/google/gerrit/server/cache/h2",
+ "//java/com/google/gerrit/server/cache/mem",
"//java/com/google/gerrit/server/git/receive",
"//java/com/google/gerrit/server/restapi",
"//java/com/google/gerrit/server/schema",
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index ffec375..17c7319 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -37,7 +37,8 @@
import com.google.gerrit.server.account.Realm;
import com.google.gerrit.server.account.externalids.ExternalIdModule;
import com.google.gerrit.server.cache.CacheRemovalListener;
-import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
+import com.google.gerrit.server.cache.h2.H2CacheModule;
+import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeKindCacheImpl;
import com.google.gerrit.server.change.MergeabilityCacheImpl;
@@ -51,13 +52,13 @@
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.GitReceivePackGroups;
import com.google.gerrit.server.config.GitUploadPackGroups;
+import com.google.gerrit.server.config.SysExecutorModule;
import com.google.gerrit.server.extensions.events.EventUtil;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.extensions.events.RevisionCreated;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.git.SearchingChangeCacheImpl;
import com.google.gerrit.server.git.TagCache;
-import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
import com.google.gerrit.server.notedb.NoteDbModule;
import com.google.gerrit.server.patch.DiffExecutorModule;
@@ -105,7 +106,7 @@
protected void configure() {
install(reviewDbModule);
install(new DiffExecutorModule());
- install(new ReceiveCommitsExecutorModule());
+ install(new SysExecutorModule());
install(BatchUpdate.module());
install(PatchListCacheImpl.module());
@@ -154,7 +155,8 @@
install(new BatchGitModule());
install(new DefaultPermissionBackendModule());
- install(new DefaultCacheFactory.Module());
+ install(new DefaultMemoryCacheModule());
+ install(new H2CacheModule());
install(new ExternalIdModule());
install(new GroupModule());
install(new NoteDbModule(cfg));
diff --git a/java/com/google/gerrit/pgm/util/LogFileCompressor.java b/java/com/google/gerrit/pgm/util/LogFileCompressor.java
index de39839..8d04be8 100644
--- a/java/com/google/gerrit/pgm/util/LogFileCompressor.java
+++ b/java/com/google/gerrit/pgm/util/LogFileCompressor.java
@@ -67,7 +67,7 @@
if (!enabled) {
return;
}
- //compress log once and then schedule compression every day at 11:00pm
+ // compress log once and then schedule compression every day at 11:00pm
queue.getDefaultQueue().execute(compressor);
ZoneId zone = ZoneId.systemDefault();
LocalDateTime now = LocalDateTime.now(zone);
diff --git a/java/com/google/gerrit/reviewdb/client/RefNames.java b/java/com/google/gerrit/reviewdb/client/RefNames.java
index 3739fd4..fd2fb56 100644
--- a/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -258,6 +258,11 @@
return isRefsGroups(ref) || isRefsDeletedGroups(ref) || REFS_GROUPNAMES.equals(ref);
}
+ /** Whether the ref is the configuration branch, i.e. {@code refs/meta/config}, for a project. */
+ public static boolean isConfigRef(String ref) {
+ return REFS_CONFIG.equals(ref);
+ }
+
static Integer parseShardedRefPart(String name) {
if (name == null) {
return null;
diff --git a/java/com/google/gerrit/server/AnonymousUser.java b/java/com/google/gerrit/server/AnonymousUser.java
index c96d61a..91d2d05 100644
--- a/java/com/google/gerrit/server/AnonymousUser.java
+++ b/java/com/google/gerrit/server/AnonymousUser.java
@@ -27,6 +27,12 @@
}
@Override
+ public Object getCacheKey() {
+ // Treat all anonymous users as a single user
+ return "anonymous";
+ }
+
+ @Override
public String toString() {
return "ANONYMOUS";
}
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 94f5062..70b3d68 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -62,6 +62,7 @@
"//lib:soy",
"//lib:tukaani-xz",
"//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/bouncycastle:bcpkix-neverlink",
"//lib/bouncycastle:bcprov-neverlink",
"//lib/commons:codec",
diff --git a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
index 22d6167..4ee8d33 100644
--- a/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
+++ b/java/com/google/gerrit/server/CreateGroupPermissionSyncer.java
@@ -46,7 +46,7 @@
* <p>During the transition phase, we have to keep these permissions in sync with the global
* capabilities that serve as the source of truth.
*
- * <p><This class implements a one-way synchronization from the the global {@code CREATE_GROUP}
+ * <p><This class implements a one-way synchronization from the global {@code CREATE_GROUP}
* capability in {@code All-Projects} to a {@code CREATE} permission on {@code refs/groups/*} in
* {@code All-Users}.
*/
diff --git a/java/com/google/gerrit/server/CurrentUser.java b/java/com/google/gerrit/server/CurrentUser.java
index ace06c5..eb3a3fd 100644
--- a/java/com/google/gerrit/server/CurrentUser.java
+++ b/java/com/google/gerrit/server/CurrentUser.java
@@ -90,6 +90,12 @@
*/
public abstract GroupMembership getEffectiveGroups();
+ /**
+ * Returns a unique identifier for this user that is intended to be used as a cache key. Returned
+ * object should to implement {@code equals()} and {@code hashCode()} for effective caching.
+ */
+ public abstract Object getCacheKey();
+
/** Unique name of the user on this server, if one has been assigned. */
public Optional<String> getUserName() {
return Optional.empty();
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutor.java b/java/com/google/gerrit/server/FanOutExecutor.java
similarity index 72%
copy from java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutor.java
copy to java/com/google/gerrit/server/FanOutExecutor.java
index ee83a2c..a489890 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutor.java
+++ b/java/com/google/gerrit/server/FanOutExecutor.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2012 The Android Open Source Project
+// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,15 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.git.receive;
+package com.google.gerrit.server;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.google.inject.BindingAnnotation;
import java.lang.annotation.Retention;
-import java.util.concurrent.ExecutorService;
-/** Marker on the global {@link ExecutorService} used by {@link ReceiveCommits}. */
+/**
+ * Marker on the global {@code ThreadPoolExecutor} used to do parallel work from a serving thread.
+ */
@Retention(RUNTIME)
@BindingAnnotation
-public @interface ReceiveCommitsExecutor {}
+public @interface FanOutExecutor {}
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index 8379a7c..023e8e3 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -382,6 +382,11 @@
return effectiveGroups;
}
+ @Override
+ public Object getCacheKey() {
+ return getAccountId();
+ }
+
public PersonIdent newRefLogIdent() {
return newRefLogIdent(new Date(), TimeZone.getDefault());
}
diff --git a/java/com/google/gerrit/server/InternalUser.java b/java/com/google/gerrit/server/InternalUser.java
index 821a0c6..381819d 100644
--- a/java/com/google/gerrit/server/InternalUser.java
+++ b/java/com/google/gerrit/server/InternalUser.java
@@ -36,6 +36,11 @@
}
@Override
+ public String getCacheKey() {
+ return "internal";
+ }
+
+ @Override
public boolean isInternalUser() {
return true;
}
diff --git a/java/com/google/gerrit/server/ModuleImpl.java b/java/com/google/gerrit/server/ModuleImpl.java
new file mode 100644
index 0000000..1614755
--- /dev/null
+++ b/java/com/google/gerrit/server/ModuleImpl.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target(TYPE)
+@Inherited
+/**
+ * Use this annotation to mark module as being swappable with implementation from {@code
+ * gerrit.installModule}. Note that module with this annotation shouldn't be part of circular
+ * dependency with any existing module.
+ */
+public @interface ModuleImpl {
+ String name();
+}
diff --git a/java/com/google/gerrit/server/ModuleOverloader.java b/java/com/google/gerrit/server/ModuleOverloader.java
new file mode 100644
index 0000000..7083e6d
--- /dev/null
+++ b/java/com/google/gerrit/server/ModuleOverloader.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.inject.Module;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class ModuleOverloader {
+ public static List<Module> override(List<Module> modules, List<Module> overrideCandidates) {
+ if (overrideCandidates == null || overrideCandidates.isEmpty()) {
+ return modules;
+ }
+
+ // group candidates by annotation existence
+ Map<Boolean, List<Module>> grouped =
+ overrideCandidates
+ .stream()
+ .collect(
+ Collectors.groupingBy(m -> m.getClass().getAnnotation(ModuleImpl.class) != null));
+
+ // add all non annotated libs to modules list
+ List<Module> libs = grouped.get(Boolean.FALSE);
+ if (libs != null) {
+ modules.addAll(libs);
+ }
+
+ List<Module> overrides = grouped.get(Boolean.TRUE);
+ if (overrides == null) {
+ return modules;
+ }
+
+ // swipe cache implementation with alternative provided in lib
+ return modules
+ .stream()
+ .map(
+ m -> {
+ ModuleImpl a = m.getClass().getAnnotation(ModuleImpl.class);
+ if (a == null) {
+ return m;
+ }
+ return overrides
+ .stream()
+ .filter(
+ o ->
+ o.getClass()
+ .getAnnotation(ModuleImpl.class)
+ .name()
+ .equalsIgnoreCase(a.name()))
+ .findFirst()
+ .orElse(m);
+ })
+ .collect(Collectors.toList());
+ }
+
+ private ModuleOverloader() {}
+}
diff --git a/java/com/google/gerrit/server/PeerDaemonUser.java b/java/com/google/gerrit/server/PeerDaemonUser.java
index 8a8b67a..b27e05c 100644
--- a/java/com/google/gerrit/server/PeerDaemonUser.java
+++ b/java/com/google/gerrit/server/PeerDaemonUser.java
@@ -40,6 +40,11 @@
return GroupMembership.EMPTY;
}
+ @Override
+ public Object getCacheKey() {
+ return getRemoteAddress();
+ }
+
public SocketAddress getRemoteAddress() {
return peer;
}
diff --git a/java/com/google/gerrit/server/account/AccountCache.java b/java/com/google/gerrit/server/account/AccountCache.java
index b6ca1cb..17493bf 100644
--- a/java/com/google/gerrit/server/account/AccountCache.java
+++ b/java/com/google/gerrit/server/account/AccountCache.java
@@ -16,7 +16,9 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
+import java.util.Map;
import java.util.Optional;
+import java.util.Set;
/** Caches important (but small) account state to avoid database hits. */
public interface AccountCache {
@@ -31,6 +33,20 @@
Optional<AccountState> get(Account.Id accountId);
/**
+ * Returns a {@code Map} of {@code Account.Id} to {@code AccountState} for the given account IDs.
+ * If not cached yet the accounts are loaded. If an account can't be loaded (e.g. because it is
+ * missing), the entry will be missing from the result.
+ *
+ * <p>Loads accounts in parallel if applicable.
+ *
+ * @param accountIds IDs of the account that should be retrieved
+ * @return {@code Map} of {@code Account.Id} to {@code AccountState} instances for the given
+ * account IDs, if an account can't be loaded (e.g. because it is missing), the entry will be
+ * missing from the result
+ */
+ Map<Account.Id, AccountState> get(Set<Account.Id> accountIds);
+
+ /**
* Returns an {@code AccountState} instance for the given account ID. If not cached yet the
* account is loaded. Returns an empty {@code AccountState} instance to represent a missing
* account.
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 105a457..0648f9f 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -18,9 +18,11 @@
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.cache.CacheModule;
@@ -31,8 +33,16 @@
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -60,15 +70,18 @@
private final AllUsersName allUsersName;
private final ExternalIds externalIds;
private final LoadingCache<Account.Id, Optional<AccountState>> byId;
+ private final ExecutorService executor;
@Inject
AccountCacheImpl(
AllUsersName allUsersName,
ExternalIds externalIds,
- @Named(BYID_NAME) LoadingCache<Account.Id, Optional<AccountState>> byId) {
+ @Named(BYID_NAME) LoadingCache<Account.Id, Optional<AccountState>> byId,
+ @FanOutExecutor ExecutorService executor) {
this.allUsersName = allUsersName;
this.externalIds = externalIds;
this.byId = byId;
+ this.executor = executor;
}
@Override
@@ -92,13 +105,47 @@
}
@Override
+ public Map<Account.Id, AccountState> get(Set<Account.Id> accountIds) {
+ Map<Account.Id, AccountState> accountStates = new HashMap<>(accountIds.size());
+ List<Callable<Optional<AccountState>>> callables = new ArrayList<>();
+ for (Account.Id accountId : accountIds) {
+ Optional<AccountState> state = byId.getIfPresent(accountId);
+ if (state != null) {
+ // The value is in-memory, so we just get the state
+ state.ifPresent(s -> accountStates.put(accountId, s));
+ } else {
+ // Queue up a callable so that we can load accounts in parallel
+ callables.add(() -> get(accountId));
+ }
+ }
+ if (callables.isEmpty()) {
+ return accountStates;
+ }
+
+ List<Future<Optional<AccountState>>> futures;
+ try {
+ futures = executor.invokeAll(callables);
+ } catch (InterruptedException e) {
+ log.error("Cannot load AccountStates", e);
+ return ImmutableMap.of();
+ }
+ for (Future<Optional<AccountState>> f : futures) {
+ try {
+ f.get().ifPresent(s -> accountStates.put(s.getAccount().getId(), s));
+ } catch (InterruptedException | ExecutionException e) {
+ log.error("Cannot load AccountState", e);
+ }
+ }
+ return accountStates;
+ }
+
+ @Override
public Optional<AccountState> getByUsername(String username) {
try {
- ExternalId extId = externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, username));
- if (extId == null) {
- return Optional.empty();
- }
- return get(extId.accountId());
+ return externalIds
+ .get(ExternalId.Key.create(SCHEME_USERNAME, username))
+ .map(e -> get(e.accountId()))
+ .orElseGet(Optional::empty);
} catch (IOException | ConfigInvalidException e) {
log.warn("Cannot load AccountState for username " + username, e);
return null;
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index d54cbbc..009623a 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -112,8 +112,7 @@
/** @return user identified by this external identity string */
public Optional<Account.Id> lookup(String externalId) throws AccountException {
try {
- ExternalId extId = externalIds.get(ExternalId.Key.parse(externalId));
- return extId != null ? Optional.of(extId.accountId()) : Optional.empty();
+ return externalIds.get(ExternalId.Key.parse(externalId)).map(ExternalId::accountId);
} catch (IOException | ConfigInvalidException e) {
throw new AccountException("Cannot lookup account " + externalId, e);
}
@@ -136,32 +135,33 @@
throw e;
}
try {
- ExternalId id = externalIds.get(who.getExternalIdKey());
- if (id == null) {
+ Optional<ExternalId> optionalExtId = externalIds.get(who.getExternalIdKey());
+ if (!optionalExtId.isPresent()) {
if (who.getUserName().isPresent()) {
ExternalId.Key key = ExternalId.Key.create(SCHEME_USERNAME, who.getUserName().get());
- ExternalId existingId = externalIds.get(key);
- if (existingId != null) {
+ Optional<ExternalId> existingId = externalIds.get(key);
+ if (existingId.isPresent()) {
// An inconsistency is detected in the database, having a record for scheme "username:"
// but no record for scheme "gerrit:". Try to recover by linking
// "gerrit:" identity to the existing account.
log.warn(
"User {} already has an account; link new identity to the existing account.",
who.getUserName());
- return link(existingId.accountId(), who);
+ return link(existingId.get().accountId(), who);
}
}
// New account, automatically create and return.
- log.info("External ID not found. Attempting to create new account.");
+ log.debug("External ID not found. Attempting to create new account.");
return create(who);
}
- Optional<AccountState> accountState = byIdCache.get(id.accountId());
+ ExternalId extId = optionalExtId.get();
+ Optional<AccountState> accountState = byIdCache.get(extId.accountId());
if (!accountState.isPresent()) {
log.error(
- String.format(
- "Authentication with external ID %s failed. Account %s doesn't exist.",
- id.key().get(), id.accountId().get()));
+ "Authentication with external ID {} failed. Account {} doesn't exist.",
+ extId.key().get(),
+ extId.accountId().get());
throw new AccountException("Authentication error, account not found");
}
@@ -177,8 +177,8 @@
}
// return the identity to the caller.
- update(who, id);
- return new AuthResult(id.accountId(), who.getExternalIdKey(), false);
+ update(who, extId);
+ return new AuthResult(extId.accountId(), who.getExternalIdKey(), false);
} catch (OrmException | ConfigInvalidException e) {
throw new AccountException("Authentication error", e);
}
@@ -189,11 +189,11 @@
return;
}
try {
- ExternalId id = externalIds.get(authRequest.getExternalIdKey());
- if (id == null) {
+ Optional<ExternalId> extId = externalIds.get(authRequest.getExternalIdKey());
+ if (!extId.isPresent()) {
return;
}
- setInactiveFlag.deactivate(id.accountId());
+ setInactiveFlag.deactivate(extId.get().accountId());
} catch (Exception e) {
log.error(
"Unable to deactivate account "
@@ -285,9 +285,11 @@
private AuthResult create(AuthRequest who)
throws OrmException, AccountException, IOException, ConfigInvalidException {
Account.Id newId = new Account.Id(sequences.nextAccountId());
+ log.debug("Assigning new Id {} to account", newId);
ExternalId extId =
ExternalId.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress());
+ log.debug("Created external Id: {}", extId);
checkEmailNotUsed(extId);
ExternalId userNameExtId =
who.getUserName().isPresent() ? createUsername(newId, who.getUserName().get()) : null;
@@ -411,16 +413,17 @@
*/
public AuthResult link(Account.Id to, AuthRequest who)
throws AccountException, OrmException, IOException, ConfigInvalidException {
- ExternalId extId = externalIds.get(who.getExternalIdKey());
- log.info("Link another authentication identity to an existing account");
- if (extId != null) {
+ Optional<ExternalId> optionalExtId = externalIds.get(who.getExternalIdKey());
+ log.debug("Link another authentication identity to an existing account");
+ if (optionalExtId.isPresent()) {
+ ExternalId extId = optionalExtId.get();
if (!extId.accountId().equals(to)) {
throw new AccountException(
"Identity '" + extId.key().get() + "' in use by another account");
}
update(who, extId);
} else {
- log.info("Linking new external ID to the existing account");
+ log.debug("Linking new external ID to the existing account");
ExternalId newExtId =
ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress());
checkEmailNotUsed(newExtId);
@@ -506,12 +509,12 @@
List<ExternalId> extIds = new ArrayList<>(extIdKeys.size());
for (ExternalId.Key extIdKey : extIdKeys) {
- ExternalId extId = externalIds.get(extIdKey);
- if (extId != null) {
- if (!extId.accountId().equals(from)) {
+ Optional<ExternalId> extId = externalIds.get(extIdKey);
+ if (extId.isPresent()) {
+ if (!extId.get().accountId().equals(from)) {
throw new AccountException("Identity '" + extIdKey.get() + "' in use by another account");
}
- extIds.add(extId);
+ extIds.add(extId.get());
} else {
throw new AccountException("Identity '" + extIdKey.get() + "' not found");
}
diff --git a/java/com/google/gerrit/server/account/AccountSshKey.java b/java/com/google/gerrit/server/account/AccountSshKey.java
index aeccc0a..f132585 100644
--- a/java/com/google/gerrit/server/account/AccountSshKey.java
+++ b/java/com/google/gerrit/server/account/AccountSshKey.java
@@ -14,76 +14,50 @@
package com.google.gerrit.server.account;
+import com.google.auto.value.AutoValue;
import com.google.common.base.Splitter;
import com.google.gerrit.reviewdb.client.Account;
-import java.io.Serializable;
import java.util.List;
-import java.util.Objects;
/** An SSH key approved for use by an {@link Account}. */
-public final class AccountSshKey {
- public static class Id implements Serializable {
- private static final long serialVersionUID = 2L;
-
- private Account.Id accountId;
- private int seq;
-
- public Id(Account.Id a, int s) {
- accountId = a;
- seq = s;
- }
-
- public Account.Id getParentKey() {
- return accountId;
- }
-
- public int get() {
- return seq;
- }
-
- public boolean isValid() {
- return seq > 0;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(accountId, seq);
- }
-
- @Override
- public boolean equals(Object obj) {
- if (!(obj instanceof Id)) {
- return false;
- }
- Id otherId = (Id) obj;
- return Objects.equals(accountId, otherId.accountId) && Objects.equals(seq, otherId.seq);
- }
+@AutoValue
+public abstract class AccountSshKey {
+ public static AccountSshKey create(Account.Id accountId, int seq, String sshPublicKey) {
+ return create(accountId, seq, sshPublicKey, true);
}
- private AccountSshKey.Id id;
- private String sshPublicKey;
- private boolean valid;
-
- public AccountSshKey(AccountSshKey.Id i, String pub) {
- id = i;
- sshPublicKey = pub.replace("\n", "").replace("\r", "");
- valid = id.isValid();
+ public static AccountSshKey createInvalid(Account.Id accountId, int seq, String sshPublicKey) {
+ return create(accountId, seq, sshPublicKey, false);
}
- public Account.Id getAccount() {
- return id.accountId;
+ public static AccountSshKey createInvalid(AccountSshKey key) {
+ return create(key.accountId(), key.seq(), key.sshPublicKey(), false);
}
- public AccountSshKey.Id getKey() {
- return id;
+ public static AccountSshKey create(
+ Account.Id accountId, int seq, String sshPublicKey, boolean valid) {
+ return new AutoValue_AccountSshKey.Builder()
+ .setAccountId(accountId)
+ .setSeq(seq)
+ .setSshPublicKey(stripOffNewLines(sshPublicKey))
+ .setValid(valid && seq > 0)
+ .build();
}
- public String getSshPublicKey() {
- return sshPublicKey;
+ private static String stripOffNewLines(String s) {
+ return s.replace("\n", "").replace("\r", "");
}
- private String getPublicKeyPart(int index, String defaultValue) {
- String s = getSshPublicKey();
+ public abstract Account.Id accountId();
+
+ public abstract int seq();
+
+ public abstract String sshPublicKey();
+
+ public abstract boolean valid();
+
+ private String publicKeyPart(int index, String defaultValue) {
+ String s = sshPublicKey();
if (s != null && s.length() > 0) {
List<String> parts = Splitter.on(' ').splitToList(s);
if (parts.size() > index) {
@@ -93,39 +67,28 @@
return defaultValue;
}
- public String getAlgorithm() {
- return getPublicKeyPart(0, "none");
+ public String algorithm() {
+ return publicKeyPart(0, "none");
}
- public String getEncodedKey() {
- return getPublicKeyPart(1, null);
+ public String encodedKey() {
+ return publicKeyPart(1, null);
}
- public String getComment() {
- return getPublicKeyPart(2, "");
+ public String comment() {
+ return publicKeyPart(2, "");
}
- public boolean isValid() {
- return valid && id.isValid();
- }
+ @AutoValue.Builder
+ abstract static class Builder {
+ public abstract Builder setAccountId(Account.Id accountId);
- public void setInvalid() {
- valid = false;
- }
+ public abstract Builder setSeq(int seq);
- @Override
- public boolean equals(Object o) {
- if (o instanceof AccountSshKey) {
- AccountSshKey other = (AccountSshKey) o;
- return Objects.equals(id, other.id)
- && Objects.equals(sshPublicKey, other.sshPublicKey)
- && Objects.equals(valid, other.valid);
- }
- return false;
- }
+ public abstract Builder setSshPublicKey(String sshPublicKey);
- @Override
- public int hashCode() {
- return Objects.hash(id, sshPublicKey, valid);
+ public abstract Builder setValid(boolean valid);
+
+ public abstract AccountSshKey build();
}
}
diff --git a/java/com/google/gerrit/server/account/AccountState.java b/java/com/google/gerrit/server/account/AccountState.java
index 5b9ea69..14a0e92 100644
--- a/java/com/google/gerrit/server/account/AccountState.java
+++ b/java/com/google/gerrit/server/account/AccountState.java
@@ -111,7 +111,6 @@
// Don't leak references to AccountConfig into the AccountState, since it holds a reference to
// an open Repository instance.
- // TODO(ekempin): Find a way to lazily compute these that doesn't hold the repo open.
ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches =
accountConfig.getProjectWatches();
GeneralPreferencesInfo generalPreferences = accountConfig.getGeneralPreferences();
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 3554b60..2f36cf2 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -27,6 +27,7 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.InternalAccountUpdate.Builder;
import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.ExternalIdNotes.ExternalIdNotesLoader;
import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -146,10 +147,17 @@
* @param accountState the account that is being updated
* @param update account update builder
*/
- void update(AccountState accountState, InternalAccountUpdate.Builder update);
+ void update(AccountState accountState, InternalAccountUpdate.Builder update) throws IOException;
static AccountUpdater join(List<AccountUpdater> updaters) {
- return (a, u) -> updaters.stream().forEach(updater -> updater.update(a, u));
+ return new AccountUpdater() {
+ @Override
+ public void update(AccountState accountState, Builder update) throws IOException {
+ for (AccountUpdater updater : updaters) {
+ updater.update(accountState, update);
+ }
+ }
+ };
}
static AccountUpdater joinConsumers(List<Consumer<InternalAccountUpdate.Builder>> consumers) {
diff --git a/java/com/google/gerrit/server/account/AuthorizedKeys.java b/java/com/google/gerrit/server/account/AuthorizedKeys.java
index 3a6c032..b392c18 100644
--- a/java/com/google/gerrit/server/account/AuthorizedKeys.java
+++ b/java/com/google/gerrit/server/account/AuthorizedKeys.java
@@ -41,8 +41,7 @@
continue;
} else if (line.startsWith(INVALID_KEY_COMMENT_PREFIX)) {
String pub = line.substring(INVALID_KEY_COMMENT_PREFIX.length());
- AccountSshKey key = new AccountSshKey(new AccountSshKey.Id(accountId, seq++), pub);
- key.setInvalid();
+ AccountSshKey key = AccountSshKey.createInvalid(accountId, seq++, pub);
keys.add(Optional.of(key));
} else if (line.startsWith(DELETED_KEY_COMMENT)) {
keys.add(Optional.empty());
@@ -50,7 +49,7 @@
} else if (line.startsWith("#")) {
continue;
} else {
- AccountSshKey key = new AccountSshKey(new AccountSshKey.Id(accountId, seq++), line);
+ AccountSshKey key = AccountSshKey.create(accountId, seq++, line);
keys.add(Optional.of(key));
}
}
@@ -61,10 +60,10 @@
StringBuilder b = new StringBuilder();
for (Optional<AccountSshKey> key : keys) {
if (key.isPresent()) {
- if (!key.get().isValid()) {
+ if (!key.get().valid()) {
b.append(INVALID_KEY_COMMENT_PREFIX);
}
- b.append(key.get().getSshPublicKey().trim());
+ b.append(key.get().sshPublicKey().trim());
} else {
b.append(DELETED_KEY_COMMENT);
}
diff --git a/java/com/google/gerrit/server/account/DefaultRealm.java b/java/com/google/gerrit/server/account/DefaultRealm.java
index 9d9cf23..dde6e81 100644
--- a/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.account;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.client.AccountFieldName;
import com.google.gerrit.extensions.client.AuthType;
@@ -33,7 +34,8 @@
private final AuthConfig authConfig;
@Inject
- DefaultRealm(EmailExpander emailExpander, Provider<Emails> emails, AuthConfig authConfig) {
+ @VisibleForTesting
+ public DefaultRealm(EmailExpander emailExpander, Provider<Emails> emails, AuthConfig authConfig) {
this.emailExpander = emailExpander;
this.emails = emails;
this.authConfig = authConfig;
diff --git a/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index 77752da..8c2bc10 100644
--- a/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -15,8 +15,10 @@
package com.google.gerrit.server.account;
import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
import com.google.common.base.Strings;
+import com.google.common.collect.Streams;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.AvatarInfo;
import com.google.gerrit.extensions.registration.DynamicItem;
@@ -32,7 +34,7 @@
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
-import java.util.Optional;
+import java.util.Map;
import java.util.Set;
@Singleton
@@ -66,11 +68,14 @@
if (options.equals(ID_ONLY)) {
return;
}
+ Set<Account.Id> ids =
+ Streams.stream(in).map(a -> new Account.Id(a._accountId)).collect(toSet());
+ Map<Account.Id, AccountState> accountStates = accountCache.get(ids);
for (AccountInfo info : in) {
Account.Id id = new Account.Id(info._accountId);
- Optional<AccountState> state = accountCache.get(id);
- if (state.isPresent()) {
- fill(info, state.get(), options);
+ AccountState state = accountStates.get(id);
+ if (state != null) {
+ fill(info, accountStates.get(id), options);
} else {
info._accountId = options.contains(FillOptions.ID) ? id.get() : null;
}
diff --git a/java/com/google/gerrit/server/account/VersionedAccountDestinations.java b/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
index 1481379..a57dc7b 100644
--- a/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
+++ b/java/com/google/gerrit/server/account/VersionedAccountDestinations.java
@@ -26,7 +26,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/** Preferences for user accounts. */
+/** User configured named destinations. */
public class VersionedAccountDestinations extends VersionedMetaData {
private static final Logger log = LoggerFactory.getLogger(VersionedAccountDestinations.class);
@@ -52,6 +52,9 @@
@Override
protected void onLoad() throws IOException, ConfigInvalidException {
+ if (revision == null) {
+ return;
+ }
String prefix = DestinationList.DIR_NAME + "/";
for (PathInfo p : getPathInfos(true)) {
if (p.fileMode == FileMode.REGULAR_FILE) {
diff --git a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
index 6a32f57..c7ffa55 100644
--- a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
+++ b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java
@@ -24,7 +24,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountSshKey.Id;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -139,8 +138,8 @@
public static class SimpleSshKeyCreator implements SshKeyCreator {
@Override
- public AccountSshKey create(Id id, String encoded) {
- return new AccountSshKey(id, encoded);
+ public AccountSshKey create(Account.Id accountId, int seq, String encoded) {
+ return AccountSshKey.create(accountId, seq, encoded);
}
}
@@ -211,14 +210,13 @@
checkLoaded();
for (Optional<AccountSshKey> key : keys) {
- if (key.isPresent() && key.get().getSshPublicKey().trim().equals(pub.trim())) {
+ if (key.isPresent() && key.get().sshPublicKey().trim().equals(pub.trim())) {
return key.get();
}
}
int seq = keys.size() + 1;
- AccountSshKey.Id keyId = new AccountSshKey.Id(accountId, seq);
- AccountSshKey key = sshKeyCreator.create(keyId, pub);
+ AccountSshKey key = sshKeyCreator.create(accountId, seq, pub);
keys.add(Optional.of(key));
return key;
}
@@ -249,9 +247,10 @@
*/
private boolean markKeyInvalid(int seq) {
checkLoaded();
- AccountSshKey key = getKey(seq);
- if (key != null && key.isValid()) {
- key.setInvalid();
+
+ Optional<AccountSshKey> key = keys.get(seq - 1);
+ if (key.isPresent() && key.get().valid()) {
+ keys.set(seq - 1, Optional.of(AccountSshKey.createInvalid(key.get())));
return true;
}
return false;
@@ -265,10 +264,10 @@
* @param newKeys the new public SSH keys
*/
public void setKeys(Collection<AccountSshKey> newKeys) {
- Ordering<AccountSshKey> o = Ordering.from(comparing(k -> k.getKey().get()));
- keys = new ArrayList<>(Collections.nCopies(o.max(newKeys).getKey().get(), Optional.empty()));
+ Ordering<AccountSshKey> o = Ordering.from(comparing(k -> k.seq()));
+ keys = new ArrayList<>(Collections.nCopies(o.max(newKeys).seq(), Optional.empty()));
for (AccountSshKey key : newKeys) {
- keys.set(key.getKey().get() - 1, Optional.of(key));
+ keys.set(key.seq() - 1, Optional.of(key));
}
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java b/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
index 8d040d6..ee6d5cd 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
@@ -27,6 +27,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -122,22 +123,21 @@
}
/** Reads and returns the specified external ID. */
- @Nullable
- ExternalId get(ExternalId.Key key) throws IOException, ConfigInvalidException {
+ Optional<ExternalId> get(ExternalId.Key key) throws IOException, ConfigInvalidException {
checkReadEnabled();
try (Repository repo = repoManager.openRepository(allUsersName)) {
- return ExternalIdNotes.loadReadOnly(repo).get(key).orElse(null);
+ return ExternalIdNotes.loadReadOnly(repo).get(key);
}
}
/** Reads and returns the specified external ID from the given revision. */
- @Nullable
- ExternalId get(ExternalId.Key key, ObjectId rev) throws IOException, ConfigInvalidException {
+ Optional<ExternalId> get(ExternalId.Key key, ObjectId rev)
+ throws IOException, ConfigInvalidException {
checkReadEnabled();
try (Repository repo = repoManager.openRepository(allUsersName)) {
- return ExternalIdNotes.loadReadOnly(repo, rev).get(key).orElse(null);
+ return ExternalIdNotes.loadReadOnly(repo, rev).get(key);
}
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIds.java b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
index 167af45..b1a59b1 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
@@ -18,11 +18,11 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.SetMultimap;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
@@ -54,14 +54,12 @@
}
/** Returns the specified external ID. */
- @Nullable
- public ExternalId get(ExternalId.Key key) throws IOException, ConfigInvalidException {
+ public Optional<ExternalId> get(ExternalId.Key key) throws IOException, ConfigInvalidException {
return externalIdReader.get(key);
}
/** Returns the specified external ID from the given revision. */
- @Nullable
- public ExternalId get(ExternalId.Key key, ObjectId rev)
+ public Optional<ExternalId> get(ExternalId.Key key, ObjectId rev)
throws IOException, ConfigInvalidException {
return externalIdReader.get(key, rev);
}
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 366ebfb..12c65ca 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -19,6 +19,7 @@
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.extensions.api.accounts.AccountApi;
+import com.google.gerrit.extensions.api.accounts.EmailApi;
import com.google.gerrit.extensions.api.accounts.EmailInput;
import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
import com.google.gerrit.extensions.api.accounts.SshKeyInput;
@@ -124,6 +125,7 @@
private final DeleteExternalIds deleteExternalIds;
private final PutStatus putStatus;
private final GetGroups getGroups;
+ private final EmailApiImpl.Factory emailApi;
@Inject
AccountApiImpl(
@@ -162,6 +164,7 @@
DeleteExternalIds deleteExternalIds,
PutStatus putStatus,
GetGroups getGroups,
+ EmailApiImpl.Factory emailApi,
@Assisted AccountResource account) {
this.account = account;
this.accountLoaderFactory = ailf;
@@ -199,6 +202,7 @@
this.deleteExternalIds = deleteExternalIds;
this.putStatus = putStatus;
this.getGroups = getGroups;
+ this.emailApi = emailApi;
}
@Override
@@ -411,6 +415,26 @@
}
@Override
+ public EmailApi createEmail(EmailInput input) throws RestApiException {
+ AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), input.email);
+ try {
+ createEmailFactory.create(input.email).apply(rsrc, input);
+ return email(rsrc.getEmail());
+ } catch (Exception e) {
+ throw asRestApiException("Cannot create email", e);
+ }
+ }
+
+ @Override
+ public EmailApi email(String email) throws RestApiException {
+ try {
+ return emailApi.create(account, email);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot parse email", e);
+ }
+ }
+
+ @Override
public void setStatus(String status) throws RestApiException {
StatusInput in = new StatusInput(status);
try {
diff --git a/java/com/google/gerrit/server/api/accounts/EmailApiImpl.java b/java/com/google/gerrit/server/api/accounts/EmailApiImpl.java
new file mode 100644
index 0000000..759f60c
--- /dev/null
+++ b/java/com/google/gerrit/server/api/accounts/EmailApiImpl.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.accounts;
+
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
+import com.google.gerrit.extensions.api.accounts.EmailApi;
+import com.google.gerrit.extensions.common.EmailInfo;
+import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.restapi.account.DeleteEmail;
+import com.google.gerrit.server.restapi.account.EmailsCollection;
+import com.google.gerrit.server.restapi.account.GetEmail;
+import com.google.gerrit.server.restapi.account.PutPreferred;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class EmailApiImpl implements EmailApi {
+ interface Factory {
+ EmailApiImpl create(AccountResource account, String email);
+ }
+
+ private final EmailsCollection emails;
+ private final GetEmail get;
+ private final DeleteEmail delete;
+ private final PutPreferred putPreferred;
+ private final AccountResource account;
+ private final String email;
+
+ @Inject
+ EmailApiImpl(
+ EmailsCollection emails,
+ GetEmail get,
+ DeleteEmail delete,
+ PutPreferred putPreferred,
+ @Assisted AccountResource account,
+ @Assisted String email) {
+ this.emails = emails;
+ this.get = get;
+ this.delete = delete;
+ this.putPreferred = putPreferred;
+ this.account = account;
+ this.email = email;
+ }
+
+ @Override
+ public EmailInfo get() throws RestApiException {
+ try {
+ return get.apply(resource());
+ } catch (Exception e) {
+ throw asRestApiException("Cannot read email", e);
+ }
+ }
+
+ @Override
+ public void delete() throws RestApiException {
+ try {
+ delete.apply(resource(), new Input());
+ } catch (Exception e) {
+ throw asRestApiException("Cannot delete email", e);
+ }
+ }
+
+ @Override
+ public void setPreferred() throws RestApiException {
+ try {
+ putPreferred.apply(resource(), new Input());
+ } catch (Exception e) {
+ throw asRestApiException(String.format("Cannot set %s as preferred email", email), e);
+ }
+ }
+
+ private AccountResource.Email resource() throws RestApiException, PermissionBackendException {
+ return emails.parse(account, IdString.fromDecoded(email));
+ }
+}
diff --git a/java/com/google/gerrit/server/api/accounts/Module.java b/java/com/google/gerrit/server/api/accounts/Module.java
index 935c4d7..15c6ddb 100644
--- a/java/com/google/gerrit/server/api/accounts/Module.java
+++ b/java/com/google/gerrit/server/api/accounts/Module.java
@@ -23,5 +23,6 @@
bind(Accounts.class).to(AccountsImpl.class);
factory(AccountApiImpl.Factory.class);
+ factory(EmailApiImpl.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index e5e2405..6184674 100644
--- a/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -351,7 +351,8 @@
@Override
public Optional<Account.Id> load(String username) throws Exception {
- return Optional.ofNullable(externalIds.get(ExternalId.Key.create(SCHEME_GERRIT, username)))
+ return externalIds
+ .get(ExternalId.Key.create(SCHEME_GERRIT, username))
.map(ExternalId::accountId);
}
}
diff --git a/java/com/google/gerrit/server/cache/CacheModule.java b/java/com/google/gerrit/server/cache/CacheModule.java
index e68eb43..0e0c16f 100644
--- a/java/com/google/gerrit/server/cache/CacheModule.java
+++ b/java/com/google/gerrit/server/cache/CacheModule.java
@@ -31,6 +31,9 @@
/** Miniature DSL to support binding {@link Cache} instances in Guice. */
public abstract class CacheModule extends FactoryModule {
+ public static final String MEMORY_MODULE = "cache-memory";
+ public static final String PERSISTENT_MODULE = "cache-persistent";
+
private static final TypeLiteral<Cache<?, ?>> ANY_CACHE = new TypeLiteral<Cache<?, ?>>() {};
/**
diff --git a/java/com/google/gerrit/server/cache/PerThreadCache.java b/java/com/google/gerrit/server/cache/PerThreadCache.java
new file mode 100644
index 0000000..32dff38
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/PerThreadCache.java
@@ -0,0 +1,134 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.Nullable;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * Caches object instances for a request as {@link ThreadLocal} in the serving thread.
+ *
+ * <p>This class is intended to cache objects that have a high instantiation cost, are specific to
+ * the current request and potentially need to be instantiated multiple times while serving a
+ * request.
+ *
+ * <p>This is different from the key-value storage in {@code CurrentUser}: {@code CurrentUser}
+ * offers a key-value storage by providing thread-safe {@code get} and {@code put} methods. Once the
+ * value is retrieved through {@code get} there is not thread-safety anymore - apart from the
+ * retrieved object guarantees. Depending on the implementation of {@code CurrentUser}, it might be
+ * shared between the request serving thread as well as sub- or background treads.
+ *
+ * <p>In comparison to that, this class guarantees thread safety even on non-thread-safe objects as
+ * its cache is tied to the serving thread only. While allowing to cache non-thread-safe objects, it
+ * has the downside of not sharing any objects with background threads or executors.
+ *
+ * <p>Lastly, this class offers a cache, that requires callers to also provide a {@code Supplier} in
+ * case the object is not present in the cache, while {@code CurrentUser} provides a storage where
+ * just retrieving stored values is a valid operation.
+ */
+public class PerThreadCache implements AutoCloseable {
+ private static final ThreadLocal<PerThreadCache> CACHE = new ThreadLocal<>();
+
+ /**
+ * Unique key for key-value mappings stored in PerThreadCache. The key is based on the value's
+ * class and a list of identifiers that in combination uniquely set the object apart form others
+ * of the same class.
+ */
+ public static final class Key<T> {
+ private final Class<T> clazz;
+ private final ImmutableList<Object> identifiers;
+
+ /**
+ * Returns a key based on the value's class and an identifier that uniquely identify the value.
+ * The identifier needs to implement {@code equals()} and {@hashCode()}.
+ */
+ public static <T> Key<T> create(Class<T> clazz, Object identifier) {
+ return new Key<>(clazz, ImmutableList.of(identifier));
+ }
+
+ /**
+ * Returns a key based on the value's class and a set of identifiers that uniquely identify the
+ * value. Identifiers need to implement {@code equals()} and {@hashCode()}.
+ */
+ public static <T> Key<T> create(Class<T> clazz, Object... identifiers) {
+ return new Key<>(clazz, ImmutableList.copyOf(identifiers));
+ }
+
+ private Key(Class<T> clazz, ImmutableList<Object> identifiers) {
+ this.clazz = clazz;
+ this.identifiers = identifiers;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(clazz, identifiers);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Key)) {
+ return false;
+ }
+ Key<?> other = (Key<?>) o;
+ return this.clazz == other.clazz && this.identifiers.equals(other.identifiers);
+ }
+ }
+
+ public static PerThreadCache create() {
+ checkState(CACHE.get() == null, "called create() twice on the same request");
+ PerThreadCache cache = new PerThreadCache();
+ CACHE.set(cache);
+ return cache;
+ }
+
+ @Nullable
+ public static PerThreadCache get() {
+ return CACHE.get();
+ }
+
+ public static <T> T getOrCompute(Key<T> key, Supplier<T> loader) {
+ PerThreadCache cache = get();
+ return cache != null ? cache.get(key, loader) : loader.get();
+ }
+
+ private final Map<Key<?>, Object> cache = Maps.newHashMapWithExpectedSize(10);
+
+ private PerThreadCache() {}
+
+ /**
+ * Returns an instance of {@code T} that was either loaded from the cache or obtained from the
+ * provided {@link Supplier}.
+ */
+ public <T> T get(Key<T> key, Supplier<T> loader) {
+ @SuppressWarnings("unchecked")
+ T value = (T) cache.get(key);
+ if (value == null) {
+ value = loader.get();
+ cache.put(key, value);
+ }
+ return value;
+ }
+
+ @Override
+ public void close() {
+ CACHE.remove();
+ }
+}
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheFactory.java b/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
index c52c232..d134adf 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
@@ -17,12 +17,11 @@
import com.google.common.cache.Cache;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
-import com.google.gerrit.server.plugins.Plugin;
public interface PersistentCacheFactory {
<K, V> Cache<K, V> build(CacheBinding<K, V> def);
<K, V> LoadingCache<K, V> build(CacheBinding<K, V> def, CacheLoader<K, V> loader);
- void onStop(Plugin plugin);
+ void onStop(String plugin);
}
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheBindingProxy.java b/java/com/google/gerrit/server/cache/h2/H2CacheBindingProxy.java
new file mode 100644
index 0000000..0d1cf20
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheBindingProxy.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.h2;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.Weigher;
+import com.google.gerrit.server.cache.CacheBinding;
+import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
+import com.google.inject.TypeLiteral;
+import java.util.concurrent.TimeUnit;
+
+class H2CacheBindingProxy<K, V> implements CacheBinding<K, V> {
+ private static final String MSG_NOT_SUPPORTED =
+ "This is read-only wrapper. Modifications are not supported";
+
+ private final CacheBinding<K, V> source;
+
+ H2CacheBindingProxy(CacheBinding<K, V> source) {
+ this.source = source;
+ }
+
+ @Override
+ public Long expireAfterWrite(TimeUnit unit) {
+ return source.expireAfterWrite(unit);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Weigher<K, V> weigher() {
+ Weigher<K, V> weigher = source.weigher();
+ if (weigher == null) {
+ return null;
+ }
+
+ // introduce weigher that performs calculations
+ // on value that is being stored not on ValueHolder
+ return (Weigher<K, V>)
+ new Weigher<K, ValueHolder<V>>() {
+ @Override
+ public int weigh(K key, ValueHolder<V> value) {
+ return weigher.weigh(key, value.value);
+ }
+ };
+ }
+
+ @Override
+ public String name() {
+ return source.name();
+ }
+
+ @Override
+ public TypeLiteral<K> keyType() {
+ return source.keyType();
+ }
+
+ @Override
+ public TypeLiteral<V> valueType() {
+ return source.valueType();
+ }
+
+ @Override
+ public long maximumWeight() {
+ return source.maximumWeight();
+ }
+
+ @Override
+ public long diskLimit() {
+ return source.diskLimit();
+ }
+
+ @Override
+ public CacheLoader<K, V> loader() {
+ return source.loader();
+ }
+
+ @Override
+ public CacheBinding<K, V> maximumWeight(long weight) {
+ throw new RuntimeException(MSG_NOT_SUPPORTED);
+ }
+
+ @Override
+ public CacheBinding<K, V> diskLimit(long limit) {
+ throw new RuntimeException(MSG_NOT_SUPPORTED);
+ }
+
+ @Override
+ public CacheBinding<K, V> expireAfterWrite(long duration, TimeUnit durationUnits) {
+ throw new RuntimeException(MSG_NOT_SUPPORTED);
+ }
+
+ @Override
+ public CacheBinding<K, V> loader(Class<? extends CacheLoader<K, V>> clazz) {
+ throw new RuntimeException(MSG_NOT_SUPPORTED);
+ }
+
+ @Override
+ public CacheBinding<K, V> weigher(Class<? extends Weigher<K, V>> clazz) {
+ throw new RuntimeException(MSG_NOT_SUPPORTED);
+ }
+}
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 1283452..2240c7d 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -21,12 +21,12 @@
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.server.cache.CacheBinding;
+import com.google.gerrit.server.cache.MemoryCacheFactory;
import com.google.gerrit.server.cache.PersistentCacheFactory;
import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.plugins.Plugin;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -50,7 +50,7 @@
class H2CacheFactory implements PersistentCacheFactory, LifecycleListener {
private static final Logger log = LoggerFactory.getLogger(H2CacheFactory.class);
- private final DefaultCacheFactory defaultFactory;
+ private final MemoryCacheFactory memCacheFactory;
private final Config config;
private final Path cacheDir;
private final List<H2CacheImpl<?, ?>> caches;
@@ -62,11 +62,11 @@
@Inject
H2CacheFactory(
- DefaultCacheFactory defaultCacheFactory,
+ MemoryCacheFactory memCacheFactory,
@GerritServerConfig Config cfg,
SitePaths site,
DynamicMap<Cache<?, ?>> cacheMap) {
- defaultFactory = defaultCacheFactory;
+ this.memCacheFactory = memCacheFactory;
config = cfg;
cacheDir = getCacheDir(site, cfg.getString("cache", null, "directory"));
h2CacheSize = cfg.getLong("cache", null, "h2CacheSize", -1);
@@ -154,21 +154,19 @@
@SuppressWarnings({"unchecked"})
@Override
- public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
- long limit = config.getLong("cache", def.name(), "diskLimit", 128 << 20);
+ public <K, V> Cache<K, V> build(CacheBinding<K, V> in) {
+ long limit = config.getLong("cache", in.name(), "diskLimit", in.diskLimit());
if (cacheDir == null || limit <= 0) {
- return defaultFactory.build(def);
+ return memCacheFactory.build(in);
}
+ H2CacheBindingProxy<K, V> def = new H2CacheBindingProxy<>(in);
SqlStore<K, V> store =
newSqlStore(def.name(), def.keyType(), limit, def.expireAfterWrite(TimeUnit.SECONDS));
H2CacheImpl<K, V> cache =
new H2CacheImpl<>(
- executor,
- store,
- def.keyType(),
- (Cache<K, ValueHolder<V>>) defaultFactory.create(def, true).build());
+ executor, store, def.keyType(), (Cache<K, ValueHolder<V>>) memCacheFactory.build(def));
synchronized (caches) {
caches.add(cache);
}
@@ -177,30 +175,31 @@
@SuppressWarnings("unchecked")
@Override
- public <K, V> LoadingCache<K, V> build(CacheBinding<K, V> def, CacheLoader<K, V> loader) {
- long limit = config.getLong("cache", def.name(), "diskLimit", def.diskLimit());
+ public <K, V> LoadingCache<K, V> build(CacheBinding<K, V> in, CacheLoader<K, V> loader) {
+ long limit = config.getLong("cache", in.name(), "diskLimit", in.diskLimit());
if (cacheDir == null || limit <= 0) {
- return defaultFactory.build(def, loader);
+ return memCacheFactory.build(in, loader);
}
+ H2CacheBindingProxy<K, V> def = new H2CacheBindingProxy<>(in);
SqlStore<K, V> store =
newSqlStore(def.name(), def.keyType(), limit, def.expireAfterWrite(TimeUnit.SECONDS));
Cache<K, ValueHolder<V>> mem =
(Cache<K, ValueHolder<V>>)
- defaultFactory
- .create(def, true)
- .build((CacheLoader<K, V>) new H2CacheImpl.Loader<>(executor, store, loader));
+ memCacheFactory.build(
+ def, (CacheLoader<K, V>) new H2CacheImpl.Loader<>(executor, store, loader));
H2CacheImpl<K, V> cache = new H2CacheImpl<>(executor, store, def.keyType(), mem);
- caches.add(cache);
+ synchronized (caches) {
+ caches.add(cache);
+ }
return cache;
}
@Override
- public void onStop(Plugin plugin) {
+ public void onStop(String plugin) {
synchronized (caches) {
- for (Map.Entry<String, Provider<Cache<?, ?>>> entry :
- cacheMap.byPlugin(plugin.getName()).entrySet()) {
+ for (Map.Entry<String, Provider<Cache<?, ?>>> entry : cacheMap.byPlugin(plugin).entrySet()) {
Cache<?, ?> cache = entry.getValue().get();
if (caches.remove(cache)) {
((H2CacheImpl<?, ?>) cache).stop();
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheModule.java b/java/com/google/gerrit/server/cache/h2/H2CacheModule.java
new file mode 100644
index 0000000..f605578
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheModule.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.h2;
+
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.ModuleImpl;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.PersistentCacheFactory;
+
+@ModuleImpl(name = CacheModule.PERSISTENT_MODULE)
+public class H2CacheModule extends LifecycleModule {
+ @Override
+ protected void configure() {
+ bind(PersistentCacheFactory.class).to(H2CacheFactory.class);
+ listener().to(H2CacheFactory.class);
+ }
+}
diff --git a/java/com/google/gerrit/server/cache/mem/BUILD b/java/com/google/gerrit/server/cache/mem/BUILD
new file mode 100644
index 0000000..ef297f1
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/mem/BUILD
@@ -0,0 +1,12 @@
+java_library(
+ name = "mem",
+ srcs = glob(["*.java"]),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/server",
+ "//lib:guava",
+ "//lib/guice",
+ "//lib/jgit/org.eclipse.jgit:jgit",
+ ],
+)
diff --git a/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
similarity index 70%
rename from java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
rename to java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
index 3566955..114e893 100644
--- a/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
+++ b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheFactory.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.cache.h2;
+package com.google.gerrit.server.cache.mem;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
@@ -20,35 +20,21 @@
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.Weigher;
-import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.cache.CacheBinding;
import com.google.gerrit.server.cache.ForwardingRemovalListener;
import com.google.gerrit.server.cache.MemoryCacheFactory;
-import com.google.gerrit.server.cache.PersistentCacheFactory;
-import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Config;
-public class DefaultCacheFactory implements MemoryCacheFactory {
- public static class Module extends LifecycleModule {
- @Override
- protected void configure() {
- factory(ForwardingRemovalListener.Factory.class);
- bind(DefaultCacheFactory.class);
- bind(MemoryCacheFactory.class).to(DefaultCacheFactory.class);
- bind(PersistentCacheFactory.class).to(H2CacheFactory.class);
- listener().to(H2CacheFactory.class);
- }
- }
-
+class DefaultMemoryCacheFactory implements MemoryCacheFactory {
private final Config cfg;
private final ForwardingRemovalListener.Factory forwardingRemovalListenerFactory;
@Inject
- public DefaultCacheFactory(
+ DefaultMemoryCacheFactory(
@GerritServerConfig Config config,
ForwardingRemovalListener.Factory forwardingRemovalListenerFactory) {
this.cfg = config;
@@ -57,16 +43,16 @@
@Override
public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
- return create(def, false).build();
+ return create(def).build();
}
@Override
public <K, V> LoadingCache<K, V> build(CacheBinding<K, V> def, CacheLoader<K, V> loader) {
- return create(def, false).build(loader);
+ return create(def).build(loader);
}
@SuppressWarnings("unchecked")
- <K, V> CacheBuilder<K, V> create(CacheBinding<K, V> def, boolean unwrapValueHolder) {
+ private <K, V> CacheBuilder<K, V> create(CacheBinding<K, V> def) {
CacheBuilder<K, V> builder = newCacheBuilder();
builder.recordStats();
builder.maximumWeight(cfg.getLong("cache", def.name(), "memoryLimit", def.maximumWeight()));
@@ -74,17 +60,7 @@
builder = builder.removalListener(forwardingRemovalListenerFactory.create(def.name()));
Weigher<K, V> weigher = def.weigher();
- if (weigher != null && unwrapValueHolder) {
- final Weigher<K, V> impl = weigher;
- weigher =
- (Weigher<K, V>)
- new Weigher<K, ValueHolder<V>>() {
- @Override
- public int weigh(K key, ValueHolder<V> value) {
- return impl.weigh(key, value.value);
- }
- };
- } else if (weigher == null) {
+ if (weigher == null) {
weigher = unitWeight();
}
builder.weigher(weigher);
diff --git a/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheModule.java b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheModule.java
new file mode 100644
index 0000000..7beb0bb
--- /dev/null
+++ b/java/com/google/gerrit/server/cache/mem/DefaultMemoryCacheModule.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache.mem;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.ModuleImpl;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.ForwardingRemovalListener;
+import com.google.gerrit.server.cache.MemoryCacheFactory;
+
+@ModuleImpl(name = CacheModule.MEMORY_MODULE)
+public class DefaultMemoryCacheModule extends FactoryModule {
+ @Override
+ protected void configure() {
+ factory(ForwardingRemovalListener.Factory.class);
+ bind(MemoryCacheFactory.class).to(DefaultMemoryCacheFactory.class);
+ }
+}
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index 4748911..a08203e 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -41,13 +41,13 @@
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.extensions.events.RevisionCreated;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidators;
-import com.google.gerrit.server.mail.SendEmailExecutor;
import com.google.gerrit.server.mail.send.CreateChangeSender;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 19c9666..82affe0 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -44,7 +44,6 @@
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Throwables;
-import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -63,6 +62,8 @@
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.LabelValue;
import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitRecord.Status;
+import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.extensions.api.changes.FixInput;
import com.google.gerrit.extensions.client.ListChangesOption;
@@ -78,6 +79,7 @@
import com.google.gerrit.extensions.common.PushCertificateInfo;
import com.google.gerrit.extensions.common.ReviewerUpdateInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
import com.google.gerrit.extensions.common.TrackingIdInfo;
import com.google.gerrit.extensions.common.VotingRangeInfo;
import com.google.gerrit.extensions.common.WebLinkInfo;
@@ -87,6 +89,10 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.index.query.QueryResult;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -99,6 +105,7 @@
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerByEmailSet;
@@ -122,7 +129,6 @@
import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.RemoveReviewerControl;
@@ -148,6 +154,10 @@
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
@@ -156,6 +166,12 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Produces {@link ChangeInfo} (which is serialized to JSON afterwards) from {@link ChangeData}.
+ *
+ * <p>This is intended to be used on request scope, but may be used for converting multiple {@link
+ * ChangeData} objects from different sources.
+ */
public class ChangeJson {
private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
@@ -202,6 +218,35 @@
ChangeJson create(Iterable<ListChangesOption> options);
}
+ @Singleton
+ private static class Metrics {
+ private final Timer0 toChangeInfoLatency;
+ private final Timer0 toChangeInfosLatency;
+ private final Timer0 formatQueryResultsLatency;
+
+ @Inject
+ Metrics(MetricMaker metricMaker) {
+ toChangeInfoLatency =
+ metricMaker.newTimer(
+ "http/server/rest_api/change_json/to_change_info_latency",
+ new Description("Latency for toChangeInfo invocations in ChangeJson")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS));
+ toChangeInfosLatency =
+ metricMaker.newTimer(
+ "http/server/rest_api/change_json/to_change_infos_latency",
+ new Description("Latency for toChangeInfos invocations in ChangeJson")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS));
+ formatQueryResultsLatency =
+ metricMaker.newTimer(
+ "http/server/rest_api/change_json/format_query_results_latency",
+ new Description("Latency for formatQueryResults invocations in ChangeJson")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS));
+ }
+ }
+
private final Provider<ReviewDb> db;
private final Provider<CurrentUser> userProvider;
private final AnonymousUser anonymous;
@@ -228,6 +273,9 @@
private final ApprovalsUtil approvalsUtil;
private final RemoveReviewerControl removeReviewerControl;
private final TrackingFooters trackingFooters;
+ private final Metrics metrics;
+ private final ExecutorService fanOutExecutor;
+
private boolean lazyLoad = true;
private AccountLoader accountLoader;
private FixInput fix;
@@ -260,6 +308,8 @@
ApprovalsUtil approvalsUtil,
RemoveReviewerControl removeReviewerControl,
TrackingFooters trackingFooters,
+ Metrics metrics,
+ @FanOutExecutor ExecutorService fanOutExecutor,
@Assisted Iterable<ListChangesOption> options) {
this.db = db;
this.userProvider = user;
@@ -285,10 +335,16 @@
this.indexes = indexes;
this.approvalsUtil = approvalsUtil;
this.removeReviewerControl = removeReviewerControl;
- this.options = Sets.immutableEnumSet(options);
this.trackingFooters = trackingFooters;
+ this.metrics = metrics;
+ this.fanOutExecutor = fanOutExecutor;
+ this.options = Sets.immutableEnumSet(options);
}
+ /**
+ * See {@link ChangeData#setLazyLoad(boolean)}. If lazyLoad is set, converting data from
+ * index-backed {@link ChangeData} will fail with an exception.
+ */
public ChangeJson lazyLoad(boolean load) {
lazyLoad = load;
return this;
@@ -360,20 +416,21 @@
public List<List<ChangeInfo>> formatQueryResults(List<QueryResult<ChangeData>> in)
throws OrmException {
- accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
- ensureLoaded(FluentIterable.from(in).transformAndConcat(QueryResult::entities));
-
- List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
- Map<Change.Id, ChangeInfo> out = new HashMap<>();
- for (QueryResult<ChangeData> r : in) {
- List<ChangeInfo> infos = toChangeInfo(out, r.entities());
- if (!infos.isEmpty() && r.more()) {
- infos.get(infos.size() - 1)._moreChanges = true;
+ try (Timer0.Context ignored = metrics.formatQueryResultsLatency.start()) {
+ accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+ List<List<ChangeInfo>> res = new ArrayList<>(in.size());
+ Map<Change.Id, ChangeInfo> cache = Maps.newHashMapWithExpectedSize(in.size());
+ for (QueryResult<ChangeData> r : in) {
+ List<ChangeInfo> infos = toChangeInfos(r.entities(), cache);
+ infos.forEach(c -> cache.put(new Change.Id(c._number), c));
+ if (!infos.isEmpty() && r.more()) {
+ infos.get(infos.size() - 1)._moreChanges = true;
+ }
+ res.add(infos);
}
- res.add(infos);
+ accountLoader.fill();
+ return res;
}
- accountLoader.fill();
- return res;
}
public List<ChangeInfo> formatChangeDatas(Collection<ChangeData> in) throws OrmException {
@@ -381,12 +438,29 @@
ensureLoaded(in);
List<ChangeInfo> out = new ArrayList<>(in.size());
for (ChangeData cd : in) {
- out.add(format(cd));
+ out.add(format(cd, Optional.empty(), false));
}
accountLoader.fill();
return out;
}
+ private static Collection<SubmitRequirementInfo> requirementsFor(ChangeData cd) {
+ Collection<SubmitRequirementInfo> reqInfos = new ArrayList<>();
+ for (SubmitRecord submitRecord : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)) {
+ if (submitRecord.requirements == null) {
+ continue;
+ }
+ for (SubmitRequirement requirement : submitRecord.requirements) {
+ reqInfos.add(requirementToInfo(requirement, submitRecord.status));
+ }
+ }
+ return reqInfos;
+ }
+
+ private static SubmitRequirementInfo requirementToInfo(SubmitRequirement req, Status status) {
+ return new SubmitRequirementInfo(status.name(), req.fallbackText(), req.type(), req.data());
+ }
+
private void ensureLoaded(Iterable<ChangeData> all) throws OrmException {
if (lazyLoad) {
ChangeData.ensureChangeLoaded(all);
@@ -410,37 +484,54 @@
return options.contains(option);
}
- private List<ChangeInfo> toChangeInfo(Map<Change.Id, ChangeInfo> out, List<ChangeData> changes) {
- List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
- for (ChangeData cd : changes) {
- ChangeInfo i = out.get(cd.getId());
- if (i == null) {
- try {
- i = toChangeInfo(cd, Optional.empty());
- } catch (PatchListNotAvailableException
- | GpgException
- | OrmException
- | IOException
- | PermissionBackendException
- | RuntimeException e) {
- if (has(CHECK)) {
- i = checkOnly(cd);
- } else if (e instanceof NoSuchChangeException) {
- log.info(
- "NoSuchChangeException: Omitting corrupt change "
- + cd.getId()
- + " from results. Seems to be stale in the index.");
- continue;
- } else {
- log.warn("Omitting corrupt change " + cd.getId() + " from results", e);
- continue;
+ private List<ChangeInfo> toChangeInfos(
+ List<ChangeData> changes, Map<Change.Id, ChangeInfo> cache) {
+ try (Timer0.Context ignored = metrics.toChangeInfosLatency.start()) {
+ // Create a list of formatting calls that can be called sequentially or in parallel
+ List<Callable<Optional<ChangeInfo>>> formattingCalls = new ArrayList<>(changes.size());
+ for (ChangeData cd : changes) {
+ formattingCalls.add(
+ () -> {
+ ChangeInfo i = cache.get(cd.getId());
+ if (i != null) {
+ return Optional.of(i);
+ }
+ try {
+ ensureLoaded(Collections.singleton(cd));
+ return Optional.of(format(cd, Optional.empty(), false));
+ } catch (OrmException | RuntimeException e) {
+ log.warn("Omitting corrupt change " + cd.getId() + " from results", e);
+ return Optional.empty();
+ }
+ });
+ }
+
+ long numProjects = changes.stream().map(c -> c.project()).distinct().count();
+ if (!lazyLoad || changes.size() < 3 || numProjects < 2) {
+ // Format these changes in the request thread as the multithreading overhead would be too
+ // high.
+ List<ChangeInfo> result = new ArrayList<>(changes.size());
+ for (Callable<Optional<ChangeInfo>> c : formattingCalls) {
+ try {
+ c.call().ifPresent(result::add);
+ } catch (Exception e) {
+ log.warn("Omitting change due to exception", e);
}
}
- out.put(cd.getId(), i);
+ return result;
}
- info.add(i);
+
+ // Format the changes in parallel on the executor
+ List<ChangeInfo> result = new ArrayList<>(changes.size());
+ try {
+ for (Future<Optional<ChangeInfo>> f : fanOutExecutor.invokeAll(formattingCalls)) {
+ f.get().ifPresent(result::add);
+ }
+ } catch (InterruptedException | ExecutionException e) {
+ throw new IllegalStateException(e);
+ }
+ return result;
}
- return info;
}
private ChangeInfo checkOnly(ChangeData cd) {
@@ -489,6 +580,14 @@
private ChangeInfo toChangeInfo(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
throws PatchListNotAvailableException, GpgException, OrmException, PermissionBackendException,
IOException {
+ try (Timer0.Context ignored = metrics.toChangeInfoLatency.start()) {
+ return toChangeInfoImpl(cd, limitToPsId);
+ }
+ }
+
+ private ChangeInfo toChangeInfoImpl(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
+ throws PatchListNotAvailableException, GpgException, OrmException, PermissionBackendException,
+ IOException {
ChangeInfo out = new ChangeInfo();
CurrentUser user = userProvider.get();
@@ -503,7 +602,6 @@
}
}
- PermissionBackend.ForChange perm = permissionBackendForChange(user, cd);
Change in = cd.change();
out.project = in.getProject().get();
out.branch = in.getDest().getShortName();
@@ -555,13 +653,15 @@
out.reviewed = cd.isReviewedBy(user.getAccountId()) ? true : null;
}
- out.labels = labelsFor(perm, cd, has(LABELS), has(DETAILED_LABELS));
+ out.labels = labelsFor(cd, has(LABELS), has(DETAILED_LABELS));
+ out.requirements = requirementsFor(cd);
if (out.labels != null && has(DETAILED_LABELS)) {
// If limited to specific patch sets but not the current patch set, don't
// list permitted labels, since users can't vote on those patch sets.
if (user.isIdentifiedUser()
&& (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId()))) {
+ PermissionBackend.ForChange perm = permissionBackendForChange(user, cd);
out.permittedLabels =
cd.change().getStatus() != Change.Status.ABANDONED
? permittedLabels(perm, cd)
@@ -664,8 +764,7 @@
return cd.submitRecords(SUBMIT_RULE_OPTIONS_LENIENT);
}
- private Map<String, LabelInfo> labelsFor(
- PermissionBackend.ForChange perm, ChangeData cd, boolean standard, boolean detailed)
+ private Map<String, LabelInfo> labelsFor(ChangeData cd, boolean standard, boolean detailed)
throws OrmException, PermissionBackendException {
if (!standard && !detailed) {
return null;
@@ -674,21 +773,17 @@
LabelTypes labelTypes = cd.getLabelTypes();
Map<String, LabelWithStatus> withStatus =
cd.change().getStatus() == Change.Status.MERGED
- ? labelsForSubmittedChange(perm, cd, labelTypes, standard, detailed)
- : labelsForUnsubmittedChange(perm, cd, labelTypes, standard, detailed);
+ ? labelsForSubmittedChange(cd, labelTypes, standard, detailed)
+ : labelsForUnsubmittedChange(cd, labelTypes, standard, detailed);
return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label));
}
private Map<String, LabelWithStatus> labelsForUnsubmittedChange(
- PermissionBackend.ForChange perm,
- ChangeData cd,
- LabelTypes labelTypes,
- boolean standard,
- boolean detailed)
+ ChangeData cd, LabelTypes labelTypes, boolean standard, boolean detailed)
throws OrmException, PermissionBackendException {
Map<String, LabelWithStatus> labels = initLabels(cd, labelTypes, standard);
if (detailed) {
- setAllApprovals(perm, cd, labels);
+ setAllApprovals(cd, labels);
}
for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
LabelType type = labelTypes.byLabel(e.getKey());
@@ -773,8 +868,7 @@
}
}
- private void setAllApprovals(
- PermissionBackend.ForChange basePerm, ChangeData cd, Map<String, LabelWithStatus> labels)
+ private void setAllApprovals(ChangeData cd, Map<String, LabelWithStatus> labels)
throws OrmException, PermissionBackendException {
Change.Status status = cd.change().getStatus();
checkState(
@@ -797,7 +891,7 @@
LabelTypes labelTypes = cd.getLabelTypes();
for (Account.Id accountId : allUsers) {
- PermissionBackend.ForChange perm = basePerm.user(userFactory.create(accountId));
+ PermissionBackend.ForChange perm = permissionBackendForChange(accountId, cd);
Map<String, VotingRangeInfo> pvr = getPermittedVotingRanges(permittedLabels(perm, cd));
for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
LabelType lt = labelTypes.byLabel(e.getKey());
@@ -880,11 +974,7 @@
}
private Map<String, LabelWithStatus> labelsForSubmittedChange(
- PermissionBackend.ForChange basePerm,
- ChangeData cd,
- LabelTypes labelTypes,
- boolean standard,
- boolean detailed)
+ ChangeData cd, LabelTypes labelTypes, boolean standard, boolean detailed)
throws OrmException, PermissionBackendException {
Set<Account.Id> allUsers = new HashSet<>();
if (detailed) {
@@ -943,7 +1033,7 @@
Map<String, ApprovalInfo> byLabel = Maps.newHashMapWithExpectedSize(labels.size());
Map<String, VotingRangeInfo> pvr = Collections.emptyMap();
if (detailed) {
- PermissionBackend.ForChange perm = basePerm.user(userFactory.create(accountId));
+ PermissionBackend.ForChange perm = permissionBackendForChange(accountId, cd);
pvr = getPermittedVotingRanges(permittedLabels(perm, cd));
for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
ApprovalInfo ai = approvalInfo(accountId, 0, null, null, null);
@@ -1223,7 +1313,6 @@
throws PatchListNotAvailableException, GpgException, OrmException, IOException,
PermissionBackendException {
Map<String, RevisionInfo> res = new LinkedHashMap<>();
- Boolean isWorldReadable = null;
try (Repository repo = openRepoIfNecessary(cd.project());
RevWalk rw = newRevWalk(repo)) {
for (PatchSet in : map.values()) {
@@ -1237,12 +1326,7 @@
want = id.equals(cd.change().currentPatchSetId());
}
if (want) {
- if (isWorldReadable == null) {
- isWorldReadable = isWorldReadable(cd);
- }
- res.put(
- in.getRevision().get(),
- toRevisionInfo(cd, in, repo, rw, false, changeInfo, isWorldReadable));
+ res.put(in.getRevision().get(), toRevisionInfo(cd, in, repo, rw, false, changeInfo));
}
}
return res;
@@ -1282,7 +1366,7 @@
accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
try (Repository repo = openRepoIfNecessary(cd.project());
RevWalk rw = newRevWalk(repo)) {
- RevisionInfo rev = toRevisionInfo(cd, in, repo, rw, true, null, isWorldReadable(cd));
+ RevisionInfo rev = toRevisionInfo(cd, in, repo, rw, true, null);
accountLoader.fill();
return rev;
}
@@ -1294,9 +1378,9 @@
@Nullable Repository repo,
@Nullable RevWalk rw,
boolean fillCommit,
- @Nullable ChangeInfo changeInfo,
- boolean isWorldReadable)
- throws PatchListNotAvailableException, GpgException, OrmException, IOException {
+ @Nullable ChangeInfo changeInfo)
+ throws PatchListNotAvailableException, GpgException, OrmException, IOException,
+ PermissionBackendException {
Change c = cd.change();
RevisionInfo out = new RevisionInfo();
out.isCurrent = in.getId().equals(c.currentPatchSetId());
@@ -1304,7 +1388,7 @@
out.ref = in.getRefName();
out.created = in.getCreatedOn();
out.uploader = accountLoader.get(in.getUploader());
- out.fetch = makeFetchMap(cd, in, isWorldReadable);
+ out.fetch = makeFetchMap(cd, in);
out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in);
out.description = in.getDescription();
@@ -1394,7 +1478,8 @@
return info;
}
- private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in, boolean isWorldReadable) {
+ private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
+ throws PermissionBackendException, OrmException, IOException {
Map<String, FetchInfo> r = new LinkedHashMap<>();
for (DynamicMap.Entry<DownloadScheme> e : downloadSchemes) {
String schemeName = e.getExportName();
@@ -1403,7 +1488,7 @@
|| (scheme.isAuthRequired() && !userProvider.get().isIdentifiedUser())) {
continue;
}
- if (!scheme.isAuthSupported() && !isWorldReadable) {
+ if (!scheme.isAuthSupported() && !isWorldReadable(cd)) {
continue;
}
@@ -1457,14 +1542,23 @@
label.all.add(approval);
}
+ private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd)
+ throws OrmException {
+ return permissionBackendForChange(permissionBackend.user(user).database(db), cd);
+ }
+
+ private PermissionBackend.ForChange permissionBackendForChange(Account.Id user, ChangeData cd)
+ throws OrmException {
+ return permissionBackendForChange(permissionBackend.absentUser(user).database(db), cd);
+ }
+
/**
* @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed
* from either an index-backed or a database-backed {@link ChangeData} depending on {@code
* lazyload}.
*/
- private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd)
- throws OrmException {
- PermissionBackend.WithUser withUser = permissionBackend.user(user).database(db);
+ private PermissionBackend.ForChange permissionBackendForChange(
+ PermissionBackend.WithUser withUser, ChangeData cd) throws OrmException {
return lazyLoad
? withUser.change(cd)
: withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index 05f8fc0..6286a2f 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -27,7 +27,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.mail.SendEmailExecutor;
+import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.mail.send.CommentSender;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
diff --git a/java/com/google/gerrit/server/change/FileContentUtil.java b/java/com/google/gerrit/server/change/FileContentUtil.java
index ff5fb0b..00b7a88 100644
--- a/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -34,7 +34,7 @@
import eu.medsea.mimeutil.MimeType;
import java.io.IOException;
import java.io.OutputStream;
-import java.util.Random;
+import java.security.SecureRandom;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.eclipse.jgit.errors.LargeObjectException;
@@ -42,7 +42,6 @@
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectLoader;
-import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -57,7 +56,7 @@
private static final String X_GIT_GITLINK = "x-git/gitlink";
private static final int MAX_SIZE = 5 << 20;
private static final String ZIP_TYPE = "application/zip";
- private static final Random rng = new Random();
+ private static final SecureRandom rng = new SecureRandom();
private final GitRepositoryManager repoManager;
private final FileTypeRegistry registry;
@@ -104,35 +103,35 @@
throws IOException, ResourceNotFoundException {
try (RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(revstr);
- ObjectReader reader = rw.getObjectReader();
- TreeWalk tw = TreeWalk.forPath(reader, path, commit.getTree());
- if (tw == null) {
- throw new ResourceNotFoundException();
- }
+ try (TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), path, commit.getTree())) {
+ if (tw == null) {
+ throw new ResourceNotFoundException();
+ }
- org.eclipse.jgit.lib.FileMode mode = tw.getFileMode(0);
- ObjectId id = tw.getObjectId(0);
- if (mode == org.eclipse.jgit.lib.FileMode.GITLINK) {
- return BinaryResult.create(id.name()).setContentType(X_GIT_GITLINK).base64();
- }
+ org.eclipse.jgit.lib.FileMode mode = tw.getFileMode(0);
+ ObjectId id = tw.getObjectId(0);
+ if (mode == org.eclipse.jgit.lib.FileMode.GITLINK) {
+ return BinaryResult.create(id.name()).setContentType(X_GIT_GITLINK).base64();
+ }
- ObjectLoader obj = repo.open(id, OBJ_BLOB);
- byte[] raw;
- try {
- raw = obj.getCachedBytes(MAX_SIZE);
- } catch (LargeObjectException e) {
- raw = null;
- }
+ ObjectLoader obj = repo.open(id, OBJ_BLOB);
+ byte[] raw;
+ try {
+ raw = obj.getCachedBytes(MAX_SIZE);
+ } catch (LargeObjectException e) {
+ raw = null;
+ }
- String type;
- if (mode == org.eclipse.jgit.lib.FileMode.SYMLINK) {
- type = X_GIT_SYMLINK;
- } else {
- type = registry.getMimeType(path, raw).toString();
- type = resolveContentType(project, path, FileMode.FILE, type);
- }
+ String type;
+ if (mode == org.eclipse.jgit.lib.FileMode.SYMLINK) {
+ type = X_GIT_SYMLINK;
+ } else {
+ type = registry.getMimeType(path, raw).toString();
+ type = resolveContentType(project, path, FileMode.FILE, type);
+ }
- return asBinaryResult(raw, obj).setContentType(type).base64();
+ return asBinaryResult(raw, obj).setContentType(type).base64();
+ }
}
}
@@ -166,30 +165,30 @@
}
commit = rw.parseCommit(commit.getParent(parent - 1));
}
- ObjectReader reader = rw.getObjectReader();
- TreeWalk tw = TreeWalk.forPath(reader, path, commit.getTree());
- if (tw == null) {
- throw new ResourceNotFoundException();
- }
+ try (TreeWalk tw = TreeWalk.forPath(rw.getObjectReader(), path, commit.getTree())) {
+ if (tw == null) {
+ throw new ResourceNotFoundException();
+ }
- int mode = tw.getFileMode(0).getObjectType();
- if (mode != Constants.OBJ_BLOB) {
- throw new ResourceNotFoundException();
- }
+ int mode = tw.getFileMode(0).getObjectType();
+ if (mode != Constants.OBJ_BLOB) {
+ throw new ResourceNotFoundException();
+ }
- ObjectId id = tw.getObjectId(0);
- ObjectLoader obj = repo.open(id, OBJ_BLOB);
- byte[] raw;
- try {
- raw = obj.getCachedBytes(MAX_SIZE);
- } catch (LargeObjectException e) {
- raw = null;
- }
+ ObjectId id = tw.getObjectId(0);
+ ObjectLoader obj = repo.open(id, OBJ_BLOB);
+ byte[] raw;
+ try {
+ raw = obj.getCachedBytes(MAX_SIZE);
+ } catch (LargeObjectException e) {
+ raw = null;
+ }
- MimeType contentType = registry.getMimeType(path, raw);
- return registry.isSafeInline(contentType)
- ? wrapBlob(path, obj, raw, contentType, suffix)
- : zipBlob(path, obj, commit, suffix);
+ MimeType contentType = registry.getMimeType(path, raw);
+ return registry.isSafeInline(contentType)
+ ? wrapBlob(path, obj, raw, contentType, suffix)
+ : zipBlob(path, obj, commit, suffix);
+ }
}
}
diff --git a/java/com/google/gerrit/server/change/IncludedInResolver.java b/java/com/google/gerrit/server/change/IncludedInResolver.java
index 88bf893..658c91c 100644
--- a/java/com/google/gerrit/server/change/IncludedInResolver.java
+++ b/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -146,7 +146,7 @@
* <li>after = commits with time >= target.getCommitTime()
* </ul>
*
- * Each of the before/after lists is sorted by the the commit time.
+ * Each of the before/after lists is sorted by the commit time.
*
* @param before
* @param after
diff --git a/java/com/google/gerrit/server/change/RevisionResource.java b/java/com/google/gerrit/server/change/RevisionResource.java
index 3ddfc63..deb5022 100644
--- a/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/java/com/google/gerrit/server/change/RevisionResource.java
@@ -34,19 +34,29 @@
public static final TypeLiteral<RestView<RevisionResource>> REVISION_KIND =
new TypeLiteral<RestView<RevisionResource>>() {};
+ public static RevisionResource createNonCachable(ChangeResource change, PatchSet ps) {
+ return new RevisionResource(change, ps, Optional.empty(), false);
+ }
+
private final ChangeResource change;
private final PatchSet ps;
private final Optional<ChangeEdit> edit;
- private boolean cacheable = true;
+ private final boolean cacheable;
public RevisionResource(ChangeResource change, PatchSet ps) {
this(change, ps, Optional.empty());
}
public RevisionResource(ChangeResource change, PatchSet ps, Optional<ChangeEdit> edit) {
+ this(change, ps, edit, true);
+ }
+
+ private RevisionResource(
+ ChangeResource change, PatchSet ps, Optional<ChangeEdit> edit, boolean cachable) {
this.change = change;
this.ps = ps;
this.edit = edit;
+ this.cacheable = cachable;
}
public boolean isCacheable() {
@@ -98,12 +108,6 @@
return getChangeResource().getUser();
}
- public RevisionResource doNotCache() {
- // TODO(hanwen): return a copy so cacheable can be final.
- cacheable = false;
- return this;
- }
-
public Optional<ChangeEdit> getEdit() {
return edit;
}
diff --git a/java/com/google/gerrit/server/config/CapabilityConstants.java b/java/com/google/gerrit/server/config/CapabilityConstants.java
index 5025892..961dbbd 100644
--- a/java/com/google/gerrit/server/config/CapabilityConstants.java
+++ b/java/com/google/gerrit/server/config/CapabilityConstants.java
@@ -43,4 +43,5 @@
public String viewConnections;
public String viewPlugins;
public String viewQueue;
+ public String viewAccess;
}
diff --git a/java/com/google/gerrit/server/update/ChangeUpdateExecutor.java b/java/com/google/gerrit/server/config/ChangeUpdateExecutor.java
similarity index 91%
rename from java/com/google/gerrit/server/update/ChangeUpdateExecutor.java
rename to java/com/google/gerrit/server/config/ChangeUpdateExecutor.java
index 1d957cf..4c9e5f0 100644
--- a/java/com/google/gerrit/server/update/ChangeUpdateExecutor.java
+++ b/java/com/google/gerrit/server/config/ChangeUpdateExecutor.java
@@ -12,11 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.update;
+package com.google.gerrit.server.config;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.server.update.BatchUpdate;
import com.google.inject.BindingAnnotation;
import java.lang.annotation.Retention;
diff --git a/java/com/google/gerrit/server/config/ConfigKey.java b/java/com/google/gerrit/server/config/ConfigKey.java
new file mode 100644
index 0000000..aa4ffb0
--- /dev/null
+++ b/java/com/google/gerrit/server/config/ConfigKey.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.config;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.common.Nullable;
+
+@AutoValue
+public abstract class ConfigKey {
+ public abstract String section();
+
+ @Nullable
+ public abstract String subsection();
+
+ public abstract String name();
+
+ public static ConfigKey create(String section, String subsection, String name) {
+ return new AutoValue_ConfigKey(section, subsection, name);
+ }
+
+ public static ConfigKey create(String section, String name) {
+ return new AutoValue_ConfigKey(section, null, name);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(section()).append(".");
+ if (subsection() != null) {
+ sb.append(subsection()).append(".");
+ }
+ sb.append(name());
+ return sb.toString();
+ }
+}
diff --git a/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
new file mode 100644
index 0000000..9bd4533
--- /dev/null
+++ b/java/com/google/gerrit/server/config/ConfigUpdatedEvent.java
@@ -0,0 +1,214 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.config;
+
+import com.google.common.collect.ImmutableList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.commons.lang.StringUtils;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * This event is produced by {@link GerritServerConfigReloader} and forwarded to callers
+ * implementing {@link GerritConfigListener}.
+ *
+ * <p>The event intends to:
+ *
+ * <p>1. Help the callers figure out if any action should be taken, depending on which entries are
+ * updated in gerrit.config.
+ *
+ * <p>2. Provide the callers with a mechanism to accept/reject the entries of interest: @see
+ * accept(Set<ConfigKey> entries), @see accept(String section), @see reject(Set<ConfigKey> entries)
+ * (+ various overloaded versions of these)
+ */
+public class ConfigUpdatedEvent {
+ private final Config oldConfig;
+ private final Config newConfig;
+
+ public ConfigUpdatedEvent(Config oldConfig, Config newConfig) {
+ this.oldConfig = oldConfig;
+ this.newConfig = newConfig;
+ }
+
+ public Config getOldConfig() {
+ return this.oldConfig;
+ }
+
+ public Config getNewConfig() {
+ return this.newConfig;
+ }
+
+ public Update accept(ConfigKey entry) {
+ return accept(Collections.singleton(entry));
+ }
+
+ public Update accept(Set<ConfigKey> entries) {
+ return createUpdate(entries, UpdateResult.APPLIED);
+ }
+
+ public Update accept(String section) {
+ Set<ConfigKey> entries = getEntriesFromSection(oldConfig, section);
+ entries.addAll(getEntriesFromSection(newConfig, section));
+ return createUpdate(entries, UpdateResult.APPLIED);
+ }
+
+ public Update reject(Set<ConfigKey> entries) {
+ return createUpdate(entries, UpdateResult.REJECTED);
+ }
+
+ private static Set<ConfigKey> getEntriesFromSection(Config config, String section) {
+ Set<ConfigKey> res = new LinkedHashSet<>();
+ for (String name : config.getNames(section, true)) {
+ res.add(ConfigKey.create(section, name));
+ }
+ for (String sub : config.getSubsections(section)) {
+ for (String name : config.getNames(section, sub, true)) {
+ res.add(ConfigKey.create(section, sub, name));
+ }
+ }
+ return res;
+ }
+
+ private Update createUpdate(Set<ConfigKey> entries, UpdateResult updateResult) {
+ Update update = new Update(updateResult);
+ entries
+ .stream()
+ .filter(this::isValueUpdated)
+ .forEach(
+ key -> {
+ update.addConfigUpdate(
+ new ConfigUpdateEntry(
+ key,
+ oldConfig.getString(key.section(), key.subsection(), key.name()),
+ newConfig.getString(key.section(), key.subsection(), key.name())));
+ });
+ return update;
+ }
+
+ public boolean isSectionUpdated(String section) {
+ Set<ConfigKey> entries = getEntriesFromSection(oldConfig, section);
+ entries.addAll(getEntriesFromSection(newConfig, section));
+ return isEntriesUpdated(entries);
+ }
+
+ public boolean isValueUpdated(String section, String subsection, String name) {
+ return !Objects.equals(
+ oldConfig.getString(section, subsection, name),
+ newConfig.getString(section, subsection, name));
+ }
+
+ public boolean isValueUpdated(ConfigKey key) {
+ return isValueUpdated(key.section(), key.subsection(), key.name());
+ }
+
+ public boolean isValueUpdated(String section, String name) {
+ return isValueUpdated(section, null, name);
+ }
+
+ public boolean isEntriesUpdated(Set<ConfigKey> entries) {
+ for (ConfigKey entry : entries) {
+ if (isValueUpdated(entry.section(), entry.subsection(), entry.name())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public enum UpdateResult {
+ APPLIED,
+ REJECTED;
+
+ @Override
+ public String toString() {
+ return StringUtils.capitalize(name().toLowerCase());
+ }
+ }
+
+ /**
+ * One Accepted/Rejected Update have one or more config updates (ConfigUpdateEntry) tied to it.
+ */
+ public static class Update {
+ private UpdateResult result;
+ private final Set<ConfigUpdateEntry> configUpdates;
+
+ public Update(UpdateResult result) {
+ this.configUpdates = new LinkedHashSet<>();
+ this.result = result;
+ }
+
+ public UpdateResult getResult() {
+ return result;
+ }
+
+ public List<ConfigUpdateEntry> getConfigUpdates() {
+ return ImmutableList.copyOf(configUpdates);
+ }
+
+ public void addConfigUpdate(ConfigUpdateEntry entry) {
+ this.configUpdates.add(entry);
+ }
+ }
+
+ public enum ConfigEntryType {
+ ADDED,
+ REMOVED,
+ MODIFIED,
+ UNMODIFIED
+ }
+
+ public static class ConfigUpdateEntry {
+ public final ConfigKey key;
+ public final String oldVal;
+ public final String newVal;
+
+ public ConfigUpdateEntry(ConfigKey key, String oldVal, String newVal) {
+ this.key = key;
+ this.oldVal = oldVal;
+ this.newVal = newVal;
+ }
+
+ /** Note: The toString() is used to format the output from @see ReloadConfig. */
+ @Override
+ public String toString() {
+ switch (getUpdateType()) {
+ case ADDED:
+ return String.format("+ %s = %s", key, newVal);
+ case MODIFIED:
+ return String.format("* %s = [%s => %s]", key, oldVal, newVal);
+ case REMOVED:
+ return String.format("- %s = %s", key, oldVal);
+ case UNMODIFIED:
+ return String.format(" %s = %s", key, newVal);
+ default:
+ throw new IllegalStateException("Unexpected UpdateType: " + getUpdateType().name());
+ }
+ }
+
+ public ConfigEntryType getUpdateType() {
+ if (oldVal == null && newVal != null) {
+ return ConfigEntryType.ADDED;
+ }
+ if (oldVal != null && newVal == null) {
+ return ConfigEntryType.REMOVED;
+ }
+ if (Objects.equals(oldVal, newVal)) {
+ return ConfigEntryType.UNMODIFIED;
+ }
+ return ConfigEntryType.MODIFIED;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/config/GerritConfigListener.java b/java/com/google/gerrit/server/config/GerritConfigListener.java
new file mode 100644
index 0000000..337a962
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GerritConfigListener.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import java.util.EventListener;
+import java.util.List;
+
+/**
+ * Implementations of the GerritConfigListener interface expects to react GerritServerConfig
+ * updates. @see ConfigUpdatedEvent.
+ */
+@ExtensionPoint
+public interface GerritConfigListener extends EventListener {
+ List<ConfigUpdatedEvent.Update> configUpdated(ConfigUpdatedEvent event);
+}
diff --git a/java/com/google/gerrit/server/config/GerritConfigListenerHelper.java b/java/com/google/gerrit/server/config/GerritConfigListenerHelper.java
new file mode 100644
index 0000000..1dfa3fc
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GerritConfigListenerHelper.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
+
+public class GerritConfigListenerHelper {
+ public static GerritConfigListener acceptIfChanged(ConfigKey... keys) {
+ return e ->
+ e.isEntriesUpdated(ImmutableSet.copyOf(keys))
+ ? Collections.singletonList(e.accept(ImmutableSet.copyOf(keys)))
+ : Collections.emptyList();
+ }
+}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 1084a49..5da5d1a 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -169,6 +169,7 @@
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryProcessor;
import com.google.gerrit.server.query.change.ConflictsCacheImpl;
+import com.google.gerrit.server.restapi.change.SuggestReviewers;
import com.google.gerrit.server.restapi.config.ConfigRestModule;
import com.google.gerrit.server.restapi.group.GroupModule;
import com.google.gerrit.server.rules.DefaultSubmitRule;
@@ -284,6 +285,8 @@
bind(TransferConfig.class);
bind(GcConfig.class);
+ DynamicSet.setOf(binder(), GerritConfigListener.class);
+
bind(ChangeCleanupConfig.class);
bind(AccountDeactivator.class);
@@ -370,6 +373,8 @@
DynamicMap.mapOf(binder(), DownloadCommand.class);
DynamicMap.mapOf(binder(), CloneCommand.class);
DynamicMap.mapOf(binder(), ReviewerSuggestion.class);
+ DynamicSet.bind(binder(), GerritConfigListener.class)
+ .toInstance(SuggestReviewers.configListener());
DynamicSet.setOf(binder(), ExternalIncludedIn.class);
DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
DynamicSet.setOf(binder(), PatchSetWebLink.class);
@@ -422,9 +427,8 @@
bind(AccountManager.class);
- bind(new TypeLiteral<List<CommentLinkInfo>>() {})
- .toProvider(CommentLinkProvider.class)
- .in(SINGLETON);
+ bind(new TypeLiteral<List<CommentLinkInfo>>() {}).toProvider(CommentLinkProvider.class);
+ DynamicSet.bind(binder(), GerritConfigListener.class).to(CommentLinkProvider.class);
bind(ReloadPluginListener.class)
.annotatedWith(UniqueAnnotations.create())
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigModule.java b/java/com/google/gerrit/server/config/GerritServerConfigModule.java
index a93d1f2..25ee759 100644
--- a/java/com/google/gerrit/server/config/GerritServerConfigModule.java
+++ b/java/com/google/gerrit/server/config/GerritServerConfigModule.java
@@ -76,8 +76,7 @@
bind(TrackingFooters.class).toProvider(TrackingFootersProvider.class).in(SINGLETON);
bind(Config.class)
.annotatedWith(GerritServerConfig.class)
- .toProvider(GerritServerConfigProvider.class)
- .in(SINGLETON);
+ .toProvider(GerritServerConfigProvider.class);
bind(SecureStore.class).toProvider(SecureStoreProvider.class).in(SINGLETON);
}
}
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigProvider.java b/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
index 82fb6ec..e02bf1c 100644
--- a/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
+++ b/java/com/google/gerrit/server/config/GerritServerConfigProvider.java
@@ -22,6 +22,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
+import com.google.inject.Singleton;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -36,25 +37,45 @@
/**
* Provides {@link Config} annotated with {@link GerritServerConfig}.
*
- * <p>Note that this class is not a singleton, so the few callers that need a reloaded-on-demand
- * config can inject a {@code GerritServerConfigProvider}. However, most callers won't need this,
- * and will just inject {@code @GerritServerConfig Config} directly, which is bound as a singleton
- * in {@link GerritServerConfigModule}.
+ * <p>To react on config updates, the caller should implement @see GerritConfigListener.
+ *
+ * <p>The few callers that need a reloaded-on-demand config can inject a {@code
+ * GerritServerConfigProvider} and request the lastest config with fetchLatestConfig().
*/
+@Singleton
public class GerritServerConfigProvider implements Provider<Config> {
private static final Logger log = LoggerFactory.getLogger(GerritServerConfigProvider.class);
private final SitePaths site;
private final SecureStore secureStore;
+ private final Object lock = new Object();
+
+ private GerritConfig gerritConfig;
+
@Inject
GerritServerConfigProvider(SitePaths site, SecureStore secureStore) {
this.site = site;
this.secureStore = secureStore;
+ this.gerritConfig = loadConfig();
}
@Override
public Config get() {
+ synchronized (lock) {
+ return gerritConfig;
+ }
+ }
+
+ protected ConfigUpdatedEvent updateConfig() {
+ synchronized (lock) {
+ Config oldConfig = gerritConfig;
+ gerritConfig = loadConfig();
+ return new ConfigUpdatedEvent(oldConfig, gerritConfig);
+ }
+ }
+
+ public GerritConfig loadConfig() {
FileBasedConfig baseConfig = loadConfig(null, site.gerrit_config);
if (!baseConfig.getFile().exists()) {
log.info("No " + site.gerrit_config.toAbsolutePath() + "; assuming defaults");
diff --git a/java/com/google/gerrit/server/config/GerritServerConfigReloader.java b/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
new file mode 100644
index 0000000..61adadd
--- /dev/null
+++ b/java/com/google/gerrit/server/config/GerritServerConfigReloader.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Issues a configuration reload from the GerritServerConfigProvider and notify all listeners. */
+@Singleton
+public class GerritServerConfigReloader {
+ private static final Logger log = LoggerFactory.getLogger(GerritServerConfigReloader.class);
+
+ private final GerritServerConfigProvider configProvider;
+ private final DynamicSet<GerritConfigListener> configListeners;
+
+ @Inject
+ GerritServerConfigReloader(
+ GerritServerConfigProvider configProvider, DynamicSet<GerritConfigListener> configListeners) {
+ this.configProvider = configProvider;
+ this.configListeners = configListeners;
+ }
+
+ /**
+ * Reloads the Gerrit Server Configuration from disk. Synchronized to ensure that one issued
+ * reload is fully completed before a new one starts.
+ */
+ public List<ConfigUpdatedEvent.Update> reloadConfig() {
+ log.info("Starting server configuration reload");
+ List<ConfigUpdatedEvent.Update> updates = fireUpdatedConfigEvent(configProvider.updateConfig());
+ log.info("Server configuration reload completed succesfully");
+ return updates;
+ }
+
+ public List<ConfigUpdatedEvent.Update> fireUpdatedConfigEvent(ConfigUpdatedEvent event) {
+ ArrayList<ConfigUpdatedEvent.Update> result = new ArrayList<>();
+ for (GerritConfigListener configListener : configListeners) {
+ result.addAll(configListener.configUpdated(event));
+ }
+ return result;
+ }
+}
diff --git a/java/com/google/gerrit/server/config/GerritServerId.java b/java/com/google/gerrit/server/config/GerritServerId.java
index 87dc44a..237f18c 100644
--- a/java/com/google/gerrit/server/config/GerritServerId.java
+++ b/java/com/google/gerrit/server/config/GerritServerId.java
@@ -22,7 +22,8 @@
/**
* Marker on a string holding a unique identifier for the server.
*
- * <p>This value is generated on first use and stored in {@code $site_path/etc/uuid}.
+ * <p>This value is generated on first use and stored in {@code gerrit.serverId} in {@code
+ * gerrit.config}.
*/
@Retention(RUNTIME)
@BindingAnnotation
diff --git a/java/com/google/gerrit/server/config/ProjectConfigEntry.java b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
index 39ff327..4a72ffc 100644
--- a/java/com/google/gerrit/server/config/ProjectConfigEntry.java
+++ b/java/com/google/gerrit/server/config/ProjectConfigEntry.java
@@ -104,7 +104,7 @@
this(displayName, defaultValue, null);
}
- //For inheritable boolean use 'LIST' type with InheritableBoolean
+ // For inheritable boolean use 'LIST' type with InheritableBoolean
public ProjectConfigEntry(String displayName, boolean defaultValue, String description) {
this(
displayName,
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutor.java b/java/com/google/gerrit/server/config/ReceiveCommitsExecutor.java
similarity index 88%
rename from java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutor.java
rename to java/com/google/gerrit/server/config/ReceiveCommitsExecutor.java
index ee83a2c..16d389c 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutor.java
+++ b/java/com/google/gerrit/server/config/ReceiveCommitsExecutor.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.git.receive;
+package com.google.gerrit.server.config;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -20,7 +20,7 @@
import java.lang.annotation.Retention;
import java.util.concurrent.ExecutorService;
-/** Marker on the global {@link ExecutorService} used by {@link ReceiveCommits}. */
+/** Marker on the global {@link ExecutorService} used by {@code ReceiveCommits}. */
@Retention(RUNTIME)
@BindingAnnotation
public @interface ReceiveCommitsExecutor {}
diff --git a/java/com/google/gerrit/server/mail/SendEmailExecutor.java b/java/com/google/gerrit/server/config/SendEmailExecutor.java
similarity index 95%
rename from java/com/google/gerrit/server/mail/SendEmailExecutor.java
rename to java/com/google/gerrit/server/config/SendEmailExecutor.java
index bfd5c17..cf90cbf 100644
--- a/java/com/google/gerrit/server/mail/SendEmailExecutor.java
+++ b/java/com/google/gerrit/server/config/SendEmailExecutor.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.mail;
+package com.google.gerrit.server.config;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutorModule.java b/java/com/google/gerrit/server/config/SysExecutorModule.java
similarity index 78%
rename from java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutorModule.java
rename to java/com/google/gerrit/server/config/SysExecutorModule.java
index 40c0aa5..b9fe34c 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsExecutorModule.java
+++ b/java/com/google/gerrit/server/config/SysExecutorModule.java
@@ -12,15 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.git.receive;
+package com.google.gerrit.server.config;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
-import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.mail.SendEmailExecutor;
-import com.google.gerrit.server.update.ChangeUpdateExecutor;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
@@ -33,10 +31,11 @@
/**
* Module providing the {@link ReceiveCommitsExecutor}.
*
- * <p>Unlike {@link ReceiveCommitsModule}, this module is intended to be installed only in top-level
- * injectors like in {@code Daemon}, not in the {@code sysInjector}.
+ * <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
+ * swapped out for the googlesource.com implementation.
*/
-public class ReceiveCommitsExecutorModule extends AbstractModule {
+public class SysExecutorModule extends AbstractModule {
@Override
protected void configure() {}
@@ -65,6 +64,17 @@
@Provides
@Singleton
+ @FanOutExecutor
+ public ExecutorService createFanOutExecutor(@GerritServerConfig Config config, WorkQueue queues) {
+ int poolSize = config.getInt("execution", null, "fanOutThreadPoolSize", 25);
+ if (poolSize == 0) {
+ return MoreExecutors.newDirectExecutorService();
+ }
+ return queues.createQueue(poolSize, "FanOut");
+ }
+
+ @Provides
+ @Singleton
@ChangeUpdateExecutor
public ListeningExecutorService createChangeUpdateExecutor(@GerritServerConfig Config config) {
int poolSize = config.getInt("receive", null, "changeUpdateThreads", 1);
diff --git a/java/com/google/gerrit/server/data/SubmitRequirementAttribute.java b/java/com/google/gerrit/server/data/SubmitRequirementAttribute.java
index 8a0ea51..3203024 100644
--- a/java/com/google/gerrit/server/data/SubmitRequirementAttribute.java
+++ b/java/com/google/gerrit/server/data/SubmitRequirementAttribute.java
@@ -14,12 +14,14 @@
package com.google.gerrit.server.data;
+import java.util.Map;
+
/**
* Represents a {@link com.google.gerrit.common.data.SubmitRequirement} that does not depend on
* Gerrit internal classes, to be serialized
*/
public class SubmitRequirementAttribute {
- public String shortReason;
- public String fullReason;
- public String label;
+ public Map<String, String> data;
+ public String type;
+ public String fallbackText;
}
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index d3d52e4..e557250 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -261,9 +261,9 @@
sa.requirements = new ArrayList<>();
for (SubmitRequirement req : submitRecord.requirements) {
SubmitRequirementAttribute re = new SubmitRequirementAttribute();
- re.shortReason = req.shortReason();
- re.fullReason = req.fullReason();
- re.label = req.label().orElse(null);
+ re.fallbackText = req.fallbackText();
+ re.type = req.type();
+ re.data = req.data();
sa.requirements.add(re);
}
}
diff --git a/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 2503ffc..61533da 100644
--- a/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -159,7 +159,7 @@
}
}
- private static class Event implements GitReferenceUpdatedListener.Event {
+ public static class Event implements GitReferenceUpdatedListener.Event {
private final String projectName;
private final String ref;
private final String oldObjectId;
diff --git a/java/com/google/gerrit/server/git/MergedByPushOp.java b/java/com/google/gerrit/server/git/MergedByPushOp.java
index 2439bb7..bb50218 100644
--- a/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -25,8 +25,8 @@
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.extensions.events.ChangeMerged;
-import com.google.gerrit.server.mail.SendEmailExecutor;
import com.google.gerrit.server.mail.send.MergedSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
diff --git a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
index 59fd58b2..da1f1ac 100644
--- a/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
+++ b/java/com/google/gerrit/server/git/meta/VersionedMetaData.java
@@ -481,10 +481,11 @@
return new byte[] {};
}
- TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree());
- if (tw != null) {
- ObjectLoader obj = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB);
- return obj.getCachedBytes(Integer.MAX_VALUE);
+ try (TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree())) {
+ if (tw != null) {
+ ObjectLoader obj = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB);
+ return obj.getCachedBytes(Integer.MAX_VALUE);
+ }
}
return new byte[] {};
}
@@ -495,9 +496,10 @@
return null;
}
- TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree());
- if (tw != null) {
- return tw.getObjectId(0);
+ try (TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree())) {
+ if (tw != null) {
+ return tw.getObjectId(0);
+ }
}
return null;
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 23e771d..b7297fa 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -24,6 +24,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ReceiveCommitsExecutor;
import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
import com.google.gerrit.server.git.MultiProgressMonitor;
import com.google.gerrit.server.git.ProjectRunnable;
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index 52b2372..4d24b4b 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -13,6 +13,7 @@
"//lib:guava",
"//lib:gwtorm",
"//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/guice",
"//lib/guice:guice-assistedinject",
"//lib/jgit/org.eclipse.jgit:jgit",
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 88fd40b..17e804f 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -19,6 +19,7 @@
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.common.FooterConstants.CHANGE_ID;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
+import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
import static com.google.gerrit.server.change.HashtagsUtil.cleanupHashtag;
import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
import static com.google.gerrit.server.git.receive.ReceiveConstants.COMMAND_REJECTION_MESSAGE_FOOTER;
@@ -35,7 +36,6 @@
import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
-import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
import com.google.common.base.Function;
@@ -497,7 +497,7 @@
// Collections populated during processing.
actualCommands = new ArrayList<>();
- errors = LinkedListMultimap.create();
+ errors = MultimapBuilder.linkedHashKeys().arrayListValues().build();
messages = new ArrayList<>();
pushOptions = LinkedListMultimap.create();
replaceByChange = new LinkedHashMap<>();
@@ -1179,6 +1179,11 @@
}
private boolean canDelete(ReceiveCommand cmd) throws PermissionBackendException {
+ if (isConfigRef(cmd.getRefName())) {
+ // Never allow to delete the meta config branch.
+ return false;
+ }
+
try {
permissions.ref(cmd.getRefName()).check(RefPermission.DELETE);
return projectState.statePermitsWrite();
@@ -1222,8 +1227,7 @@
}
actualCommands.add(cmd);
} else {
- cmd.setResult(
- REJECTED_NONFASTFORWARD, " need '" + PermissionRule.FORCE_PUSH + "' privilege.");
+ cmd.setResult(REJECTED_OTHER_REASON, "need '" + PermissionRule.FORCE_PUSH + "' privilege.");
}
}
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index d842790..3b8091c 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -43,12 +43,12 @@
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeKindCache;
import com.google.gerrit.server.change.EmailReviewComments;
+import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.extensions.events.RevisionCreated;
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.SendEmailExecutor;
import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
diff --git a/java/com/google/gerrit/server/group/testing/BUILD b/java/com/google/gerrit/server/group/testing/BUILD
index bde140b..134de78 100644
--- a/java/com/google/gerrit/server/group/testing/BUILD
+++ b/java/com/google/gerrit/server/group/testing/BUILD
@@ -5,6 +5,7 @@
testonly = 1,
srcs = glob(["*.java"]),
deps = [
+ "//java/com/google/gerrit/common:server",
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
"//lib:truth",
diff --git a/java/com/google/gerrit/server/group/testing/TestGroupBackend.java b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
new file mode 100644
index 0000000..c0efe2b
--- /dev/null
+++ b/java/com/google/gerrit/server/group/testing/TestGroupBackend.java
@@ -0,0 +1,123 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group.testing;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.project.ProjectState;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Implementation of GroupBackend for tests. */
+public class TestGroupBackend implements GroupBackend {
+ private static final String PREFIX = "testbackend:";
+
+ private final Map<AccountGroup.UUID, GroupDescription.Basic> groups = new HashMap<>();
+
+ /**
+ * Create a group by name.
+ *
+ * @param name the group name, optionally prefixed by "testbackend:".
+ * @return the created group
+ */
+ public GroupDescription.Basic create(String name) {
+ checkNotNull(name);
+ return create(new AccountGroup.UUID(name.startsWith(PREFIX) ? name : PREFIX + name));
+ }
+
+ /**
+ * Create a group by UUID.
+ *
+ * @param uuid the group UUID to add.
+ * @return the created group
+ */
+ public GroupDescription.Basic create(AccountGroup.UUID uuid) {
+ checkState(uuid.get().startsWith(PREFIX), "test group UUID must have prefix '" + PREFIX + "'");
+ if (groups.containsKey(uuid)) {
+ return groups.get(uuid);
+ }
+ GroupDescription.Basic group =
+ new GroupDescription.Basic() {
+ @Override
+ public AccountGroup.UUID getGroupUUID() {
+ return uuid;
+ }
+
+ @Override
+ public String getName() {
+ return uuid.get().substring(PREFIX.length());
+ }
+
+ @Override
+ public String getEmailAddress() {
+ return null;
+ }
+
+ @Override
+ public String getUrl() {
+ return null;
+ }
+ };
+ groups.put(uuid, group);
+ return group;
+ }
+
+ /**
+ * Remove a group. No-op if the group does not exist.
+ *
+ * @param uuid the group.
+ */
+ public void remove(AccountGroup.UUID uuid) {
+ groups.remove(uuid);
+ }
+
+ @Override
+ public boolean handles(AccountGroup.UUID uuid) {
+ if (uuid != null) {
+ String id = uuid.get();
+ return id != null && id.startsWith(PREFIX);
+ }
+ return false;
+ }
+
+ @Override
+ public GroupDescription.Basic get(AccountGroup.UUID uuid) {
+ return uuid == null ? null : groups.get(uuid);
+ }
+
+ @Override
+ public Collection<GroupReference> suggest(String name, ProjectState project) {
+ return ImmutableList.of();
+ }
+
+ @Override
+ public GroupMembership membershipsOf(IdentifiedUser user) {
+ return GroupMembership.EMPTY;
+ }
+
+ @Override
+ public boolean isVisibleToAll(AccountGroup.UUID uuid) {
+ return false;
+ }
+}
diff --git a/java/com/google/gerrit/server/index/AbstractIndexModule.java b/java/com/google/gerrit/server/index/AbstractIndexModule.java
new file mode 100644
index 0000000..12aedfd
--- /dev/null
+++ b/java/com/google/gerrit/server/index/AbstractIndexModule.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.project.ProjectIndex;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.index.account.AccountIndex;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.gerrit.server.index.group.GroupIndex;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import java.util.Map;
+import org.eclipse.jgit.lib.Config;
+
+public abstract class AbstractIndexModule extends AbstractModule {
+
+ private final int threads;
+ private final Map<String, Integer> singleVersions;
+ private final boolean onlineUpgrade;
+ private final boolean slave;
+
+ protected AbstractIndexModule(
+ Map<String, Integer> singleVersions, int threads, boolean onlineUpgrade, boolean slave) {
+ if (singleVersions != null) {
+ checkArgument(!onlineUpgrade, "online upgrade is incompatible with single version map");
+ }
+ this.singleVersions = singleVersions;
+ this.threads = threads;
+ this.onlineUpgrade = onlineUpgrade;
+ this.slave = slave;
+ }
+
+ @Override
+ protected void configure() {
+ if (slave) {
+ bind(AccountIndex.Factory.class).toInstance(AbstractIndexModule::createDummyIndexFactory);
+ bind(ChangeIndex.Factory.class).toInstance(AbstractIndexModule::createDummyIndexFactory);
+ bind(ProjectIndex.Factory.class).toInstance(AbstractIndexModule::createDummyIndexFactory);
+ } else {
+ install(
+ new FactoryModuleBuilder()
+ .implement(AccountIndex.class, getAccountIndex())
+ .build(AccountIndex.Factory.class));
+ install(
+ new FactoryModuleBuilder()
+ .implement(ChangeIndex.class, getChangeIndex())
+ .build(ChangeIndex.Factory.class));
+ install(
+ new FactoryModuleBuilder()
+ .implement(ProjectIndex.class, getProjectIndex())
+ .build(ProjectIndex.Factory.class));
+ }
+ install(
+ new FactoryModuleBuilder()
+ .implement(GroupIndex.class, getGroupIndex())
+ .build(GroupIndex.Factory.class));
+
+ install(new IndexModule(threads, slave));
+ if (singleVersions == null) {
+ install(new MultiVersionModule());
+ } else {
+ install(new SingleVersionModule(singleVersions));
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static <T> T createDummyIndexFactory(Schema<?> schema) {
+ throw new UnsupportedOperationException();
+ }
+
+ protected abstract Class<? extends AccountIndex> getAccountIndex();
+
+ protected abstract Class<? extends ChangeIndex> getChangeIndex();
+
+ protected abstract Class<? extends GroupIndex> getGroupIndex();
+
+ protected abstract Class<? extends ProjectIndex> getProjectIndex();
+
+ protected abstract Class<? extends VersionManager> getVersionManager();
+
+ @Provides
+ @Singleton
+ IndexConfig provideIndexConfig(@GerritServerConfig Config cfg) {
+ return getIndexConfig(cfg);
+ }
+
+ protected IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
+ return IndexConfig.fromConfig(cfg).separateChangeSubIndexes(true).build();
+ }
+
+ private class MultiVersionModule extends LifecycleModule {
+ @Override
+ public void configure() {
+ Class<? extends VersionManager> versionManagerClass = getVersionManager();
+ bind(VersionManager.class).to(versionManagerClass);
+ listener().to(versionManagerClass);
+ if (onlineUpgrade) {
+ listener().to(OnlineUpgrader.class);
+ }
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/index/VersionManager.java b/java/com/google/gerrit/server/index/VersionManager.java
index 620dd36..8aabb60 100644
--- a/java/com/google/gerrit/server/index/VersionManager.java
+++ b/java/com/google/gerrit/server/index/VersionManager.java
@@ -137,6 +137,16 @@
return false;
}
+ /**
+ * Tells if an index with this name is currently known or not.
+ *
+ * @param name index name
+ * @return true if index is known and can be used, otherwise false.
+ */
+ public boolean isKnownIndex(String name) {
+ return defs.get(name) != null;
+ }
+
protected <K, V, I extends Index<K, V>> void initIndex(
IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
TreeMap<Integer, Version<V>> versions = scanVersions(def, cfg);
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 6dbad6a..2f8c785 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -36,7 +36,6 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Table;
import com.google.common.primitives.Longs;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.index.FieldDef;
@@ -77,6 +76,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -657,9 +657,9 @@
}
static class StoredRequirement {
- String shortReason;
- String fullReason;
- @Nullable String label;
+ String fallbackText;
+ String type;
+ Map<String, String> data;
}
SubmitRecord.Status status;
@@ -684,9 +684,9 @@
this.requirements = new ArrayList<>(rec.requirements.size());
for (SubmitRequirement requirement : rec.requirements) {
StoredRequirement sr = new StoredRequirement();
- sr.shortReason = requirement.shortReason();
- sr.fullReason = requirement.fullReason();
- sr.label = requirement.label().orElse(null);
+ sr.type = requirement.type();
+ sr.fallbackText = requirement.fallbackText();
+ sr.data = requirement.data();
this.requirements.add(sr);
}
}
@@ -708,10 +708,13 @@
}
if (requirements != null) {
rec.requirements = new ArrayList<>(requirements.size());
- for (StoredRequirement requirement : requirements) {
+ for (StoredRequirement req : requirements) {
SubmitRequirement sr =
- new SubmitRequirement(
- requirement.shortReason, requirement.fullReason, requirement.label);
+ SubmitRequirement.builder()
+ .setType(req.type)
+ .setFallbackText(req.fallbackText)
+ .setData(req.data)
+ .build();
rec.requirements.add(sr);
}
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index 824fd4f..976813f 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.index.change;
+import static com.google.gerrit.server.query.change.ChangeStatusPredicate.closed;
import static com.google.gerrit.server.query.change.ChangeStatusPredicate.open;
import com.google.common.collect.Lists;
@@ -139,7 +140,7 @@
throws QueryParseException {
Predicate<ChangeData> s = rewriteImpl(in, opts);
if (!(s instanceof ChangeDataSource)) {
- in = Predicate.and(open(), in);
+ in = Predicate.and(Predicate.or(open(), closed()), in);
s = rewriteImpl(in, opts);
}
if (!(s instanceof ChangeDataSource)) {
diff --git a/java/com/google/gerrit/server/mail/send/AddKeySender.java b/java/com/google/gerrit/server/mail/send/AddKeySender.java
index 3622cf9..ae8ac31 100644
--- a/java/com/google/gerrit/server/mail/send/AddKeySender.java
+++ b/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -127,7 +127,7 @@
}
public String getSshKey() {
- return (sshKey != null) ? sshKey.getSshPublicKey() + "\n" : null;
+ return (sshKey != null) ? sshKey.sshPublicKey() + "\n" : null;
}
public String getGpgKeys() {
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 29fc04f..6c05ecc 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -405,12 +405,12 @@
}
@Override
- protected boolean isVisibleTo(Account.Id to) throws OrmException, PermissionBackendException {
+ protected boolean isVisibleTo(Account.Id to) throws PermissionBackendException {
return projectState.statePermitsRead()
&& args.permissionBackend
- .user(args.identifiedUserFactory.create(to))
+ .absentUser(to)
.change(changeData)
- .database(args.db.get())
+ .database(args.db)
.test(ChangePermission.READ);
}
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 0f3bcdb..f1f1778 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -34,7 +34,6 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
import com.google.gerrit.server.validators.ValidationException;
-import com.google.gwtorm.server.OrmException;
import com.google.template.soy.data.SanitizedContent;
import java.net.MalformedURLException;
import java.net.URL;
@@ -470,18 +469,17 @@
rcptTo.add(to);
add(rt, toAddress(to), override);
}
- } catch (OrmException | PermissionBackendException e) {
+ } catch (PermissionBackendException e) {
log.error("Error reading database for account: " + to, e);
}
}
/**
* @param to account.
- * @throws OrmException
* @throws PermissionBackendException
* @return whether this email is visible to the given account.
*/
- protected boolean isVisibleTo(Account.Id to) throws OrmException, PermissionBackendException {
+ protected boolean isVisibleTo(Account.Id to) throws PermissionBackendException {
return true;
}
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index a8289c4..b08e594 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -200,30 +200,31 @@
}
}
- Writer messageDataWriter = client.sendMessageData();
- if (messageDataWriter == null) {
- /* Include rejected recipient error messages here to not lose that
- * information. That piece of the puzzle is vital if zero recipients
- * are accepted and the server consequently rejects the DATA command.
- */
- throw new EmailException(
- rejected
- + "Server "
- + smtpHost
- + " rejected DATA command: "
- + client.getReplyString());
- }
+ try (Writer messageDataWriter = client.sendMessageData()) {
+ if (messageDataWriter == null) {
+ /* Include rejected recipient error messages here to not lose that
+ * information. That piece of the puzzle is vital if zero recipients
+ * are accepted and the server consequently rejects the DATA command.
+ */
+ throw new EmailException(
+ rejected
+ + "Server "
+ + smtpHost
+ + " rejected DATA command: "
+ + client.getReplyString());
+ }
- render(messageDataWriter, callerHeaders, textBody, htmlBody);
+ render(messageDataWriter, callerHeaders, textBody, htmlBody);
- if (!client.completePendingCommand()) {
- throw new EmailException(
- "Server " + smtpHost + " rejected message body: " + client.getReplyString());
- }
+ if (!client.completePendingCommand()) {
+ throw new EmailException(
+ "Server " + smtpHost + " rejected message body: " + client.getReplyString());
+ }
- client.logout();
- if (rejected.length() > 0) {
- throw new EmailException(rejected.toString());
+ client.logout();
+ if (rejected.length() > 0) {
+ throw new EmailException(rejected.toString());
+ }
}
} finally {
client.disconnect();
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index 507b652..676dbb8 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -268,7 +268,7 @@
+ P
+ ident // author
+ P
- + ident //realAuthor
+ + ident // realAuthor
+ P
+ T // writtenOn
+ 2 // side
diff --git a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java b/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
index c11aeef..b408355 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
@@ -184,7 +184,7 @@
// Reload gerrit.config/notedb.config on each migrator invocation, in case a previous
// migration in the same process modified the on-disk contents. This ensures the defaults for
// trial/autoMigrate get set correctly below.
- this.cfg = configProvider.get();
+ this.cfg = configProvider.loadConfig();
this.sitePaths = sitePaths;
this.serverIdent = serverIdent;
this.allUsers = allUsers;
@@ -615,6 +615,7 @@
log.warn(
"Change {} previously failed to rebuild;"
+ " skipping primary storage migration",
+ id,
e);
} else {
throw e;
@@ -724,6 +725,7 @@
// Only set in-memory state once it's been persisted to storage.
globalNotesMigration.setFrom(newState);
+ log.info("Migration state: {} => {}", expectedOldState, newState);
return newState;
}
diff --git a/java/com/google/gerrit/server/patch/AutoMerger.java b/java/com/google/gerrit/server/patch/AutoMerger.java
index 19568cf..e4c207b 100644
--- a/java/com/google/gerrit/server/patch/AutoMerger.java
+++ b/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -114,7 +114,7 @@
boolean couldMerge;
try {
couldMerge = m.merge(merge.getParents());
- } catch (IOException e) {
+ } catch (IOException | RuntimeException e) {
// It is not safe to continue further down in this method as throwing
// an exception most likely means that the merge tree was not created
// and m.getMergeResults() is empty. This would mean that all paths are
diff --git a/java/com/google/gerrit/server/patch/IntraLineDiffKey.java b/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
index b47bbfb..06e2c45 100644
--- a/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
+++ b/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
@@ -21,7 +21,7 @@
@AutoValue
public abstract class IntraLineDiffKey implements Serializable {
- public static final long serialVersionUID = 11L;
+ public static final long serialVersionUID = 12L;
public static IntraLineDiffKey create(ObjectId aId, ObjectId bId, Whitespace whitespace) {
return new AutoValue_IntraLineDiffKey(aId, bId, whitespace);
diff --git a/java/com/google/gerrit/server/patch/IntraLineLoader.java b/java/com/google/gerrit/server/patch/IntraLineLoader.java
index d7ecc8a..f17f0b6 100644
--- a/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -271,6 +271,7 @@
if (editsDueToRebase.contains(c) || editsDueToRebase.contains(n)) {
// Don't combine any edits which were identified as being introduced by a rebase as we would
// lose that information because of the combination.
+ j++;
continue;
}
diff --git a/java/com/google/gerrit/server/patch/PatchListKey.java b/java/com/google/gerrit/server/patch/PatchListKey.java
index 7475fec..083c142 100644
--- a/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -32,7 +32,7 @@
import org.eclipse.jgit.lib.ObjectId;
public class PatchListKey implements Serializable {
- public static final long serialVersionUID = 30L;
+ public static final long serialVersionUID = 31L;
public static final ImmutableBiMap<Whitespace, Character> WHITESPACE_TYPES =
ImmutableBiMap.of(
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index e49686a..990c318 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.permissions;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.permissions.DefaultPermissionMappings.labelPermissionName;
import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
import com.google.common.collect.Maps;
@@ -377,7 +378,7 @@
case REMOVE_REVIEWER:
case SUBMIT_AS:
- return refControl.canPerform(perm.permissionName().get());
+ return refControl.canPerform(changePermissionName(perm));
}
} catch (OrmException e) {
throw new PermissionBackendException("unavailable", e);
@@ -386,11 +387,11 @@
}
private boolean can(LabelPermission perm) {
- return !label(perm.permissionName().get()).isEmpty();
+ return !label(labelPermissionName(perm)).isEmpty();
}
private boolean can(LabelPermission.WithValue perm) {
- PermissionRange r = label(perm.permissionName().get());
+ PermissionRange r = label(labelPermissionName(perm));
if (perm.forUser() == ON_BEHALF_OF && r.isEmpty()) {
return false;
}
@@ -419,4 +420,11 @@
}
return Sets.newHashSetWithExpectedSize(permSet.size());
}
+
+ private static String changePermissionName(ChangePermission changePermission) {
+ // Within this class, it's programmer error to call this method on a
+ // ChangePermission that isn't associated with a permission name.
+ return DefaultPermissionMappings.changePermissionName(changePermission)
+ .orElseThrow(() -> new IllegalStateException("no name for " + changePermission));
+ }
}
diff --git a/java/com/google/gerrit/server/permissions/ChangePermission.java b/java/com/google/gerrit/server/permissions/ChangePermission.java
index 4b06861..6a23cdd 100644
--- a/java/com/google/gerrit/server/permissions/ChangePermission.java
+++ b/java/com/google/gerrit/server/permissions/ChangePermission.java
@@ -14,43 +14,37 @@
package com.google.gerrit.server.permissions;
-import com.google.gerrit.common.data.Permission;
-import java.util.Locale;
-import java.util.Optional;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.extensions.api.access.GerritPermission;
public enum ChangePermission implements ChangePermissionOrLabel {
- READ(Permission.READ),
+ READ,
RESTORE,
DELETE,
- ABANDON(Permission.ABANDON),
- EDIT_ASSIGNEE(Permission.EDIT_ASSIGNEE),
+ ABANDON,
+ EDIT_ASSIGNEE,
EDIT_DESCRIPTION,
- EDIT_HASHTAGS(Permission.EDIT_HASHTAGS),
- EDIT_TOPIC_NAME(Permission.EDIT_TOPIC_NAME),
- REMOVE_REVIEWER(Permission.REMOVE_REVIEWER),
- ADD_PATCH_SET(Permission.ADD_PATCH_SET),
- REBASE(Permission.REBASE),
- SUBMIT(Permission.SUBMIT),
- SUBMIT_AS(Permission.SUBMIT_AS);
+ EDIT_HASHTAGS,
+ EDIT_TOPIC_NAME,
+ REMOVE_REVIEWER,
+ ADD_PATCH_SET,
+ REBASE,
+ SUBMIT,
+ SUBMIT_AS("submit on behalf of other users");
- private final String name;
+ private final String description;
ChangePermission() {
- name = null;
+ this.description = null;
}
- ChangePermission(String name) {
- this.name = name;
- }
-
- /** @return name used in {@code project.config} permissions. */
- @Override
- public Optional<String> permissionName() {
- return Optional.ofNullable(name);
+ ChangePermission(String description) {
+ this.description = checkNotNull(description);
}
@Override
public String describeForException() {
- return toString().toLowerCase(Locale.US).replace('_', ' ');
+ return description != null ? description : GerritPermission.describeEnumValue(this);
}
}
diff --git a/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java b/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java
index 06c0d73..2824efd 100644
--- a/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java
+++ b/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java
@@ -14,13 +14,7 @@
package com.google.gerrit.server.permissions;
-import java.util.Optional;
+import com.google.gerrit.extensions.api.access.GerritPermission;
/** A {@link ChangePermission} or a {@link LabelPermission}. */
-public interface ChangePermissionOrLabel {
- /** @return name used in {@code project.config} permissions. */
- public Optional<String> permissionName();
-
- /** @return readable identifier of this permission for exception message. */
- public String describeForException();
-}
+public interface ChangePermissionOrLabel extends GerritPermission {}
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index 2c8c951..02eed30 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.permissions;
import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.permissions.DefaultPermissionMappings.globalPermissionName;
import static java.util.stream.Collectors.toSet;
import com.google.common.collect.Sets;
@@ -30,13 +31,12 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.account.CapabilityCollection;
-import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.cache.PerThreadCache;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import java.io.IOException;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
@@ -84,6 +84,11 @@
return new WithUserImpl(identifiedUser);
}
+ @Override
+ public boolean usesDefaultCapabilities() {
+ return true;
+ }
+
class WithUserImpl extends WithUser {
private final CurrentUser user;
private Boolean admin;
@@ -101,12 +106,15 @@
public ForProject project(Project.NameKey project) {
try {
ProjectState state = projectCache.checkedGet(project);
- if (state != null) {
- return projectControlFactory.create(user, state).asForProject().database(db);
- }
- return FailedPermissionBackend.project("not found", new NoSuchProjectException(project));
- } catch (IOException e) {
- return FailedPermissionBackend.project("unavailable", e);
+ ProjectControl control =
+ PerThreadCache.getOrCompute(
+ PerThreadCache.Key.create(ProjectControl.class, project, user.getCacheKey()),
+ () -> projectControlFactory.create(user, state));
+ return control.asForProject().database(db);
+ } catch (Exception e) {
+ Throwable cause = e.getCause() != null ? e.getCause() : e;
+ return FailedPermissionBackend.project(
+ "project '" + project.get() + "' is unavailable", cause);
}
}
@@ -135,7 +143,7 @@
return can((GlobalPermission) perm);
} else if (perm instanceof PluginPermission) {
PluginPermission pluginPermission = (PluginPermission) perm;
- return has(pluginPermission.permissionName())
+ return has(DefaultPermissionMappings.pluginPermissionName(pluginPermission))
|| (pluginPermission.fallBackToAdmin() && isAdmin());
}
throw new PermissionBackendException(perm + " unsupported");
@@ -153,7 +161,7 @@
case RUN_GC:
case VIEW_CACHES:
case VIEW_QUEUE:
- return has(perm.permissionName()) || can(GlobalPermission.MAINTAIN_SERVER);
+ return has(globalPermissionName(perm)) || can(GlobalPermission.MAINTAIN_SERVER);
case CREATE_ACCOUNT:
case CREATE_GROUP:
@@ -164,11 +172,12 @@
case VIEW_ALL_ACCOUNTS:
case VIEW_CONNECTIONS:
case VIEW_PLUGINS:
- return has(perm.permissionName()) || isAdmin();
+ case VIEW_ACCESS:
+ return has(globalPermissionName(perm)) || isAdmin();
case ACCESS_DATABASE:
case RUN_AS:
- return has(perm.permissionName());
+ return has(globalPermissionName(perm));
}
throw new PermissionBackendException(perm + " unsupported");
}
@@ -204,7 +213,7 @@
}
private boolean has(String permissionName) {
- return allow(capabilities().getPermission(permissionName));
+ return allow(capabilities().getPermission(checkNotNull(permissionName)));
}
private boolean allow(Collection<PermissionRule> rules) {
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
new file mode 100644
index 0000000..9593521
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
@@ -0,0 +1,169 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
+import com.google.gerrit.extensions.api.access.PluginPermission;
+import com.google.gerrit.server.permissions.LabelPermission.ForUser;
+import java.util.EnumSet;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Mappings from {@link com.google.gerrit.extensions.api.access.GerritPermission} enum instances to
+ * the permission names used by {@link DefaultPermissionBackend}.
+ *
+ * <p>These should be considered implementation details of {@code DefaultPermissionBackend}; a
+ * backend that doesn't respect the default permission model will not need to consult these.
+ * However, implementations may also choose to respect certain aspects of the default permission
+ * model, so this class is provided as public to aid those implementations.
+ */
+public class DefaultPermissionMappings {
+ private static final ImmutableBiMap<GlobalPermission, String> CAPABILITIES =
+ ImmutableBiMap.<GlobalPermission, String>builder()
+ .put(GlobalPermission.ACCESS_DATABASE, GlobalCapability.ACCESS_DATABASE)
+ .put(GlobalPermission.ADMINISTRATE_SERVER, GlobalCapability.ADMINISTRATE_SERVER)
+ .put(GlobalPermission.CREATE_ACCOUNT, GlobalCapability.CREATE_ACCOUNT)
+ .put(GlobalPermission.CREATE_GROUP, GlobalCapability.CREATE_GROUP)
+ .put(GlobalPermission.CREATE_PROJECT, GlobalCapability.CREATE_PROJECT)
+ .put(GlobalPermission.EMAIL_REVIEWERS, GlobalCapability.EMAIL_REVIEWERS)
+ .put(GlobalPermission.FLUSH_CACHES, GlobalCapability.FLUSH_CACHES)
+ .put(GlobalPermission.KILL_TASK, GlobalCapability.KILL_TASK)
+ .put(GlobalPermission.MAINTAIN_SERVER, GlobalCapability.MAINTAIN_SERVER)
+ .put(GlobalPermission.MODIFY_ACCOUNT, GlobalCapability.MODIFY_ACCOUNT)
+ .put(GlobalPermission.RUN_AS, GlobalCapability.RUN_AS)
+ .put(GlobalPermission.RUN_GC, GlobalCapability.RUN_GC)
+ .put(GlobalPermission.STREAM_EVENTS, GlobalCapability.STREAM_EVENTS)
+ .put(GlobalPermission.VIEW_ALL_ACCOUNTS, GlobalCapability.VIEW_ALL_ACCOUNTS)
+ .put(GlobalPermission.VIEW_CACHES, GlobalCapability.VIEW_CACHES)
+ .put(GlobalPermission.VIEW_CONNECTIONS, GlobalCapability.VIEW_CONNECTIONS)
+ .put(GlobalPermission.VIEW_PLUGINS, GlobalCapability.VIEW_PLUGINS)
+ .put(GlobalPermission.VIEW_QUEUE, GlobalCapability.VIEW_QUEUE)
+ .put(GlobalPermission.VIEW_ACCESS, GlobalCapability.VIEW_ACCESS)
+ .build();
+
+ static {
+ checkMapContainsAllEnumValues(CAPABILITIES, GlobalPermission.class);
+ }
+
+ private static final ImmutableBiMap<ProjectPermission, String> PROJECT_PERMISSIONS =
+ ImmutableBiMap.<ProjectPermission, String>builder()
+ .put(ProjectPermission.READ, Permission.READ)
+ .build();
+
+ private static final ImmutableBiMap<RefPermission, String> REF_PERMISSIONS =
+ ImmutableBiMap.<RefPermission, String>builder()
+ .put(RefPermission.READ, Permission.READ)
+ .put(RefPermission.CREATE, Permission.CREATE)
+ .put(RefPermission.DELETE, Permission.DELETE)
+ .put(RefPermission.UPDATE, Permission.PUSH)
+ .put(RefPermission.FORGE_AUTHOR, Permission.FORGE_AUTHOR)
+ .put(RefPermission.FORGE_COMMITTER, Permission.FORGE_COMMITTER)
+ .put(RefPermission.FORGE_SERVER, Permission.FORGE_SERVER)
+ .put(RefPermission.CREATE_TAG, Permission.CREATE_TAG)
+ .put(RefPermission.CREATE_SIGNED_TAG, Permission.CREATE_SIGNED_TAG)
+ .put(RefPermission.READ_PRIVATE_CHANGES, Permission.VIEW_PRIVATE_CHANGES)
+ .build();
+
+ private static final ImmutableBiMap<ChangePermission, String> CHANGE_PERMISSIONS =
+ ImmutableBiMap.<ChangePermission, String>builder()
+ .put(ChangePermission.READ, Permission.READ)
+ .put(ChangePermission.ABANDON, Permission.ABANDON)
+ .put(ChangePermission.EDIT_ASSIGNEE, Permission.EDIT_ASSIGNEE)
+ .put(ChangePermission.EDIT_HASHTAGS, Permission.EDIT_HASHTAGS)
+ .put(ChangePermission.EDIT_TOPIC_NAME, Permission.EDIT_TOPIC_NAME)
+ .put(ChangePermission.REMOVE_REVIEWER, Permission.REMOVE_REVIEWER)
+ .put(ChangePermission.ADD_PATCH_SET, Permission.ADD_PATCH_SET)
+ .put(ChangePermission.REBASE, Permission.REBASE)
+ .put(ChangePermission.SUBMIT, Permission.SUBMIT)
+ .put(ChangePermission.SUBMIT_AS, Permission.SUBMIT_AS)
+ .build();
+
+ private static <T extends Enum<T>> void checkMapContainsAllEnumValues(
+ ImmutableMap<T, String> actual, Class<T> clazz) {
+ Set<T> expected = EnumSet.allOf(clazz);
+ checkState(
+ actual.keySet().equals(expected),
+ "all %s values must be defined, found: %s",
+ clazz.getSimpleName(),
+ actual.keySet());
+ }
+
+ public static String globalPermissionName(GlobalPermission globalPermission) {
+ return checkNotNull(CAPABILITIES.get(globalPermission));
+ }
+
+ public static Optional<GlobalPermission> globalPermission(String capabilityName) {
+ return Optional.ofNullable(CAPABILITIES.inverse().get(capabilityName));
+ }
+
+ public static String pluginPermissionName(PluginPermission pluginPermission) {
+ return pluginPermission.pluginName() + '-' + pluginPermission.capability();
+ }
+
+ public static String globalOrPluginPermissionName(GlobalOrPluginPermission permission) {
+ return permission instanceof GlobalPermission
+ ? globalPermissionName((GlobalPermission) permission)
+ : pluginPermissionName((PluginPermission) permission);
+ }
+
+ public static Optional<String> projectPermissionName(ProjectPermission projectPermission) {
+ return Optional.ofNullable(PROJECT_PERMISSIONS.get(projectPermission));
+ }
+
+ public static Optional<ProjectPermission> projectPermission(String permissionName) {
+ return Optional.ofNullable(PROJECT_PERMISSIONS.inverse().get(permissionName));
+ }
+
+ public static Optional<String> refPermissionName(RefPermission refPermission) {
+ return Optional.ofNullable(REF_PERMISSIONS.get(refPermission));
+ }
+
+ public static Optional<RefPermission> refPermission(String permissionName) {
+ return Optional.ofNullable(REF_PERMISSIONS.inverse().get(permissionName));
+ }
+
+ public static Optional<String> changePermissionName(ChangePermission changePermission) {
+ return Optional.ofNullable(CHANGE_PERMISSIONS.get(changePermission));
+ }
+
+ public static Optional<ChangePermission> changePermission(String permissionName) {
+ return Optional.ofNullable(CHANGE_PERMISSIONS.inverse().get(permissionName));
+ }
+
+ public static String labelPermissionName(LabelPermission labelPermission) {
+ if (labelPermission.forUser() == ForUser.ON_BEHALF_OF) {
+ return Permission.forLabelAs(labelPermission.label());
+ }
+ return Permission.forLabel(labelPermission.label());
+ }
+
+ // TODO(dborowitz): Can these share a common superinterface?
+ public static String labelPermissionName(LabelPermission.WithValue labelPermission) {
+ if (labelPermission.forUser() == ForUser.ON_BEHALF_OF) {
+ return Permission.forLabelAs(labelPermission.label());
+ }
+ return Permission.forLabel(labelPermission.label());
+ }
+
+ private DefaultPermissionMappings() {}
+}
diff --git a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index 209de69..35b6e0d 100644
--- a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.permissions;
+import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -21,6 +23,7 @@
import com.google.gerrit.server.permissions.PermissionBackend.ForProject;
import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
+import com.google.gerrit.server.permissions.PermissionBackend.WithUser;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Provider;
import java.util.Collection;
@@ -36,6 +39,14 @@
* method to the throwing {@code check} or {@code test} methods.
*/
public class FailedPermissionBackend {
+ public static WithUser user(String message) {
+ return new FailedWithUser(message, null);
+ }
+
+ public static WithUser user(String message, Throwable cause) {
+ return new FailedWithUser(message, cause);
+ }
+
public static ForProject project(String message) {
return project(message, null);
}
@@ -62,6 +73,37 @@
private FailedPermissionBackend() {}
+ private static class FailedWithUser extends WithUser {
+ private final String message;
+ private final Throwable cause;
+
+ FailedWithUser(String message, Throwable cause) {
+ this.message = message;
+ this.cause = cause;
+ }
+
+ @Override
+ public CurrentUser user() {
+ throw new UnsupportedOperationException("FailedPermissionBackend is not scoped to user");
+ }
+
+ @Override
+ public ForProject project(Project.NameKey project) {
+ return new FailedProject(message, cause);
+ }
+
+ @Override
+ public void check(GlobalOrPluginPermission perm) throws PermissionBackendException {
+ throw new PermissionBackendException(message, cause);
+ }
+
+ @Override
+ public <T extends GlobalOrPluginPermission> Set<T> test(Collection<T> permSet)
+ throws PermissionBackendException {
+ throw new PermissionBackendException(message, cause);
+ }
+ }
+
private static class FailedProject extends ForProject {
private final String message;
private final Throwable cause;
diff --git a/java/com/google/gerrit/server/permissions/GlobalPermission.java b/java/com/google/gerrit/server/permissions/GlobalPermission.java
index 4d293c8..a789bd9 100644
--- a/java/com/google/gerrit/server/permissions/GlobalPermission.java
+++ b/java/com/google/gerrit/server/permissions/GlobalPermission.java
@@ -14,58 +14,46 @@
package com.google.gerrit.server.permissions;
-import com.google.common.collect.ImmutableMap;
+import static com.google.gerrit.server.permissions.DefaultPermissionMappings.globalPermission;
+
import com.google.gerrit.common.Nullable;
-import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.CapabilityScope;
import com.google.gerrit.extensions.annotations.RequiresAnyCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.api.access.GerritPermission;
import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
import com.google.gerrit.extensions.api.access.PluginPermission;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.LinkedHashSet;
-import java.util.Locale;
+import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Global server permissions built into Gerrit. */
public enum GlobalPermission implements GlobalOrPluginPermission {
- ACCESS_DATABASE(GlobalCapability.ACCESS_DATABASE),
- ADMINISTRATE_SERVER(GlobalCapability.ADMINISTRATE_SERVER),
- CREATE_ACCOUNT(GlobalCapability.CREATE_ACCOUNT),
- CREATE_GROUP(GlobalCapability.CREATE_GROUP),
- CREATE_PROJECT(GlobalCapability.CREATE_PROJECT),
- EMAIL_REVIEWERS(GlobalCapability.EMAIL_REVIEWERS),
- FLUSH_CACHES(GlobalCapability.FLUSH_CACHES),
- KILL_TASK(GlobalCapability.KILL_TASK),
- MAINTAIN_SERVER(GlobalCapability.MAINTAIN_SERVER),
- MODIFY_ACCOUNT(GlobalCapability.MODIFY_ACCOUNT),
- RUN_AS(GlobalCapability.RUN_AS),
- RUN_GC(GlobalCapability.RUN_GC),
- STREAM_EVENTS(GlobalCapability.STREAM_EVENTS),
- VIEW_ALL_ACCOUNTS(GlobalCapability.VIEW_ALL_ACCOUNTS),
- VIEW_CACHES(GlobalCapability.VIEW_CACHES),
- VIEW_CONNECTIONS(GlobalCapability.VIEW_CONNECTIONS),
- VIEW_PLUGINS(GlobalCapability.VIEW_PLUGINS),
- VIEW_QUEUE(GlobalCapability.VIEW_QUEUE);
+ ACCESS_DATABASE,
+ ADMINISTRATE_SERVER,
+ CREATE_ACCOUNT,
+ CREATE_GROUP,
+ CREATE_PROJECT,
+ EMAIL_REVIEWERS,
+ FLUSH_CACHES,
+ KILL_TASK,
+ MAINTAIN_SERVER,
+ MODIFY_ACCOUNT,
+ RUN_AS,
+ RUN_GC,
+ STREAM_EVENTS,
+ VIEW_ALL_ACCOUNTS,
+ VIEW_CACHES,
+ VIEW_CONNECTIONS,
+ VIEW_PLUGINS,
+ VIEW_QUEUE,
+ VIEW_ACCESS;
private static final Logger log = LoggerFactory.getLogger(GlobalPermission.class);
- private static final ImmutableMap<String, GlobalPermission> BY_NAME;
-
- static {
- ImmutableMap.Builder<String, GlobalPermission> m = ImmutableMap.builder();
- for (GlobalPermission p : values()) {
- m.put(p.permissionName(), p);
- }
- BY_NAME = m.build();
- }
-
- @Nullable
- public static GlobalPermission byName(String name) {
- return BY_NAME.get(name);
- }
/**
* Extracts the {@code @RequiresCapability} or {@code @RequiresAnyCapability} annotation.
@@ -121,23 +109,6 @@
return fromAnnotation(null, clazz);
}
- private final String name;
-
- GlobalPermission(String name) {
- this.name = name;
- }
-
- /** @return name used in {@code project.config} permissions. */
- @Override
- public String permissionName() {
- return name;
- }
-
- @Override
- public String describeForException() {
- return toString().toLowerCase(Locale.US).replace('_', ' ');
- }
-
private static GlobalOrPluginPermission resolve(
@Nullable String pluginName,
String capability,
@@ -160,13 +131,13 @@
throw new PermissionBackendException("cannot extract permission");
}
- GlobalPermission perm = byName(capability);
- if (perm == null) {
+ Optional<GlobalPermission> perm = globalPermission(capability);
+ if (!perm.isPresent()) {
log.error(
String.format("Class %s requires unknown capability %s", clazz.getName(), capability));
throw new PermissionBackendException("cannot extract permission");
}
- return perm;
+ return perm.get();
}
@Nullable
@@ -179,4 +150,9 @@
}
return null;
}
+
+ @Override
+ public String describeForException() {
+ return GerritPermission.describeEnumValue(this);
+ }
}
diff --git a/java/com/google/gerrit/server/permissions/LabelPermission.java b/java/com/google/gerrit/server/permissions/LabelPermission.java
index 747c997..a80cc15 100644
--- a/java/com/google/gerrit/server/permissions/LabelPermission.java
+++ b/java/com/google/gerrit/server/permissions/LabelPermission.java
@@ -20,9 +20,7 @@
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.common.data.Permission;
import com.google.gerrit.server.util.LabelVote;
-import java.util.Optional;
/** Permission representing a label. */
public class LabelPermission implements ChangePermissionOrLabel {
@@ -83,22 +81,10 @@
return name;
}
- /** @return name used in {@code project.config} permissions. */
- @Override
- public Optional<String> permissionName() {
- switch (forUser) {
- case SELF:
- return Optional.of(Permission.forLabel(name));
- case ON_BEHALF_OF:
- return Optional.of(Permission.forLabelAs(name));
- }
- return Optional.empty();
- }
-
@Override
public String describeForException() {
if (forUser == ON_BEHALF_OF) {
- return "labelAs " + name;
+ return "label on behalf of " + name;
}
return "label " + name;
}
@@ -228,22 +214,10 @@
return label.value();
}
- /** @return name used in {@code project.config} permissions. */
- @Override
- public Optional<String> permissionName() {
- switch (forUser) {
- case SELF:
- return Optional.of(Permission.forLabel(label()));
- case ON_BEHALF_OF:
- return Optional.of(Permission.forLabelAs(label()));
- }
- return Optional.empty();
- }
-
@Override
public String describeForException() {
if (forUser == ON_BEHALF_OF) {
- return "labelAs " + label.formatWithEquals();
+ return "label on behalf of " + label.formatWithEquals();
}
return "label " + label.formatWithEquals();
}
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index ed10184..3872a38 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -23,6 +23,7 @@
import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
import com.google.gerrit.extensions.conditions.BooleanCondition;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
@@ -40,6 +41,7 @@
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
@@ -113,6 +115,29 @@
public abstract WithUser absentUser(Account.Id user);
/**
+ * Check whether this {@code PermissionBackend} respects the same global capabilities as the
+ * {@link DefaultPermissionBackend}.
+ *
+ * <p>If true, then it makes sense for downstream callers to refer to built-in Gerrit capability
+ * names in user-facing error messages, for example.
+ *
+ * @return whether this is the default permission backend.
+ */
+ public boolean usesDefaultCapabilities() {
+ return false;
+ }
+
+ /**
+ * Throw {@link ResourceNotFoundException} if this backend does not use the default global
+ * capabilities.
+ */
+ public void checkUsesDefaultCapabilities() throws ResourceNotFoundException {
+ if (!usesDefaultCapabilities()) {
+ throw new ResourceNotFoundException("Gerrit capabilities not used on this server");
+ }
+ }
+
+ /**
* Bulk evaluate a set of {@link PermissionBackendCondition} for view handling.
*
* <p>Overridden implementations should call {@link PermissionBackendCondition#set(boolean)} to
@@ -256,6 +281,13 @@
allowed.add(project);
} catch (AuthException e) {
// Do not include this project in allowed.
+ } catch (PermissionBackendException e) {
+ if (e.getCause() instanceof RepositoryNotFoundException) {
+ logger.warn("Could not find repository of the project {} : ", project.get(), e);
+ // Do not include this project because doesn't exist
+ } else {
+ throw e;
+ }
}
}
return allowed;
diff --git a/java/com/google/gerrit/server/permissions/PermissionCollection.java b/java/com/google/gerrit/server/permissions/PermissionCollection.java
index e8e6030..f103462 100644
--- a/java/com/google/gerrit/server/permissions/PermissionCollection.java
+++ b/java/com/google/gerrit/server/permissions/PermissionCollection.java
@@ -132,7 +132,8 @@
// project closer to the current one come first.
sorter.sort(ref, sections);
- // For block permissions, we want a different order: first, we want to go from parent to child.
+ // For block permissions, we want a different order: first, we want to go from parent to
+ // child.
List<Map.Entry<AccessSection, Project.NameKey>> accessDescending =
Lists.reverse(Lists.newArrayList(sectionToProject.entrySet()));
diff --git a/java/com/google/gerrit/server/permissions/ProjectPermission.java b/java/com/google/gerrit/server/permissions/ProjectPermission.java
index 37f3726..3fee6cf 100644
--- a/java/com/google/gerrit/server/permissions/ProjectPermission.java
+++ b/java/com/google/gerrit/server/permissions/ProjectPermission.java
@@ -14,25 +14,26 @@
package com.google.gerrit.server.permissions;
-import com.google.gerrit.common.data.Permission;
-import java.util.Locale;
-import java.util.Optional;
+import static com.google.common.base.Preconditions.checkNotNull;
-public enum ProjectPermission {
+import com.google.gerrit.extensions.api.access.GerritPermission;
+import com.google.gerrit.reviewdb.client.RefNames;
+
+public enum ProjectPermission implements GerritPermission {
/**
* Can access at least one reference or change within the repository.
*
* <p>Checking this permission instead of {@link #READ} may require filtering to hide specific
* references or changes, which can be expensive.
*/
- ACCESS,
+ ACCESS("access at least one ref"),
/**
* Can read all references in the repository.
*
* <p>This is a stronger form of {@link #ACCESS} where no filtering is required.
*/
- READ(Permission.READ),
+ READ,
/**
* Can create at least one reference in the project.
@@ -65,16 +66,16 @@
CREATE_CHANGE,
/** Can run receive pack. */
- RUN_RECEIVE_PACK,
+ RUN_RECEIVE_PACK("run receive-pack"),
/** Can run upload pack. */
- RUN_UPLOAD_PACK,
+ RUN_UPLOAD_PACK("run upload-pack"),
/** Allow read access to refs/meta/config. */
- READ_CONFIG,
+ READ_CONFIG("read " + RefNames.REFS_CONFIG),
/** Allow write access to refs/meta/config. */
- WRITE_CONFIG,
+ WRITE_CONFIG("write " + RefNames.REFS_CONFIG),
/** Allow banning commits from Gerrit preventing pushes of these commits. */
BAN_COMMIT,
@@ -83,24 +84,20 @@
READ_REFLOG,
/** Can push to at least one reference within the repository. */
- PUSH_AT_LEAST_ONE_REF;
+ PUSH_AT_LEAST_ONE_REF("push to at least one ref");
- private final String name;
+ private final String description;
ProjectPermission() {
- name = null;
+ this.description = null;
}
- ProjectPermission(String name) {
- this.name = name;
+ ProjectPermission(String description) {
+ this.description = checkNotNull(description);
}
- /** @return name used in {@code project.config} permissions. */
- public Optional<String> permissionName() {
- return Optional.ofNullable(name);
- }
-
+ @Override
public String describeForException() {
- return toString().toLowerCase(Locale.US).replace('_', ' ');
+ return description != null ? description : GerritPermission.describeEnumValue(this);
}
}
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 8e9912c..28781ea 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -213,15 +213,6 @@
* @return {@code true} if the user specified can delete a Git ref.
*/
private boolean canDelete() {
- if (RefNames.REFS_CONFIG.equals(refName)) {
- // Never allow removal of the refs/meta/config branch.
- // Deleting the branch would destroy all Gerrit specific
- // metadata about the project, including its access rules.
- // If a project is to be removed from Gerrit, its repository
- // should be removed first.
- return false;
- }
-
switch (getUser().getAccessPath()) {
case GIT:
return canPushWithForce() || canPerform(Permission.DELETE);
@@ -476,7 +467,7 @@
return isVisible();
case CREATE:
// TODO This isn't an accurate test.
- return canPerform(perm.permissionName().get());
+ return canPerform(refPermissionName(perm));
case DELETE:
return canDelete();
case UPDATE:
@@ -500,7 +491,7 @@
case CREATE_TAG:
case CREATE_SIGNED_TAG:
- return canPerform(perm.permissionName().get());
+ return canPerform(refPermissionName(perm));
case UPDATE_BY_SUBMIT:
return projectControl.controlForRef(MagicBranch.NEW_CHANGE + refName).canSubmit(true);
@@ -524,4 +515,11 @@
throw new PermissionBackendException(perm + " unsupported");
}
}
+
+ private static String refPermissionName(RefPermission refPermission) {
+ // Within this class, it's programmer error to call this method on a
+ // RefPermission that isn't associated with a permission name.
+ return DefaultPermissionMappings.refPermissionName(refPermission)
+ .orElseThrow(() -> new IllegalStateException("no name for " + refPermission));
+ }
}
diff --git a/java/com/google/gerrit/server/permissions/RefPermission.java b/java/com/google/gerrit/server/permissions/RefPermission.java
index 87bd14c..a9f2758 100644
--- a/java/com/google/gerrit/server/permissions/RefPermission.java
+++ b/java/com/google/gerrit/server/permissions/RefPermission.java
@@ -14,22 +14,29 @@
package com.google.gerrit.server.permissions;
-import com.google.gerrit.common.data.Permission;
-import java.util.Arrays;
-import java.util.Locale;
-import java.util.Optional;
+import static com.google.common.base.Preconditions.checkNotNull;
-public enum RefPermission {
- READ(Permission.READ),
- CREATE(Permission.CREATE),
- DELETE(Permission.DELETE),
- UPDATE(Permission.PUSH),
+import com.google.gerrit.extensions.api.access.GerritPermission;
+
+public enum RefPermission implements GerritPermission {
+ READ,
+ CREATE,
+
+ /**
+ * Before checking this permission, the caller needs to verify the branch is deletable and reject
+ * early if the branch should never be deleted. For example, the refs/meta/config branch should
+ * never be deleted because deleting this branch would destroy all Gerrit specific metadata about
+ * the project, including its access rules. If a project is to be removed from Gerrit, its
+ * repository should be removed first.
+ */
+ DELETE,
+ UPDATE,
FORCE_UPDATE,
- SET_HEAD,
+ SET_HEAD("set HEAD"),
- FORGE_AUTHOR(Permission.FORGE_AUTHOR),
- FORGE_COMMITTER(Permission.FORGE_COMMITTER),
- FORGE_SERVER(Permission.FORGE_SERVER),
+ FORGE_AUTHOR,
+ FORGE_COMMITTER,
+ FORGE_SERVER,
MERGE,
/**
* Before checking this permission, the caller should verify {@code USE_SIGNED_OFF_BY} is false.
@@ -41,10 +48,10 @@
CREATE_CHANGE,
/** Create a tag. */
- CREATE_TAG(Permission.CREATE_TAG),
+ CREATE_TAG,
/** Create a signed tag. */
- CREATE_SIGNED_TAG(Permission.CREATE_SIGNED_TAG),
+ CREATE_SIGNED_TAG,
/**
* Creates changes, then also immediately submits them during {@code push}.
@@ -59,35 +66,26 @@
* Can read all private changes on the ref. Typically granted to CI systems if they should run on
* private changes.
*/
- READ_PRIVATE_CHANGES(Permission.VIEW_PRIVATE_CHANGES),
+ READ_PRIVATE_CHANGES,
/** Read access to ref's config section in {@code project.config}. */
- READ_CONFIG,
+ READ_CONFIG("read ref config"),
/** Write access to ref's config section in {@code project.config}. */
- WRITE_CONFIG;
+ WRITE_CONFIG("write ref config");
- private final String name;
+ private final String description;
RefPermission() {
- name = null;
+ this.description = null;
}
- RefPermission(String name) {
- this.name = name;
+ RefPermission(String description) {
+ this.description = checkNotNull(description);
}
- /** @return name used in {@code project.config} permissions. */
- public Optional<String> permissionName() {
- return Optional.ofNullable(name);
- }
-
+ @Override
public String describeForException() {
- return toString().toLowerCase(Locale.US).replace('_', ' ');
- }
-
- /** @return the enum constant for a given permission name if present. */
- public static Optional<RefPermission> fromName(String name) {
- return Arrays.stream(RefPermission.values()).filter(p -> name.equals(p.name)).findFirst();
+ return description != null ? description : GerritPermission.describeEnumValue(this);
}
}
diff --git a/java/com/google/gerrit/server/plugins/DelegatingClassLoader.java b/java/com/google/gerrit/server/plugins/DelegatingClassLoader.java
index daee9c7..32adb9c 100644
--- a/java/com/google/gerrit/server/plugins/DelegatingClassLoader.java
+++ b/java/com/google/gerrit/server/plugins/DelegatingClassLoader.java
@@ -31,13 +31,17 @@
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
String path = name.replace('.', '/') + ".class";
- InputStream resource = target.getResourceAsStream(path);
- if (resource != null) {
- try {
- byte[] bytes = ByteStreams.toByteArray(resource);
- return defineClass(name, bytes, 0, bytes.length);
- } catch (IOException e) {
+ try (InputStream resource = target.getResourceAsStream(path)) {
+ if (resource != null) {
+ try {
+ byte[] bytes = ByteStreams.toByteArray(resource);
+ return defineClass(name, bytes, 0, bytes.length);
+ } catch (IOException e) {
+ // throws ClassNotFoundException later
+ }
}
+ } catch (IOException e) {
+ // throws ClassNotFoundException later
}
throw new ClassNotFoundException(name);
}
diff --git a/java/com/google/gerrit/server/plugins/JarPluginProvider.java b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
index de82370..87c3df7 100644
--- a/java/com/google/gerrit/server/plugins/JarPluginProvider.java
+++ b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -136,7 +136,8 @@
urls.add(tmp.toUri().toURL());
ClassLoader pluginLoader =
- new URLClassLoader(urls.toArray(new URL[urls.size()]), PluginUtil.parentFor(type));
+ URLClassLoader.newInstance(
+ urls.toArray(new URL[urls.size()]), PluginUtil.parentFor(type));
JarScanner jarScanner = createJarScanner(tmp);
PluginConfig pluginConfig = configFactory.getFromGerritConfig(name);
diff --git a/java/com/google/gerrit/server/plugins/PluginLoader.java b/java/com/google/gerrit/server/plugins/PluginLoader.java
index 954ea29..07ffbdc 100644
--- a/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -50,7 +50,6 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
-import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@@ -79,11 +78,11 @@
private final PluginGuiceEnvironment env;
private final ServerInformationImpl srvInfoImpl;
private final PluginUser.Factory pluginUserFactory;
- private final ConcurrentMap<String, Plugin> running;
- private final ConcurrentMap<String, Plugin> disabled;
- private final Map<String, FileSnapshot> broken;
- private final Map<Plugin, CleanupHandle> cleanupHandles;
- private final Queue<Plugin> toCleanup;
+ private final ConcurrentMap<String, Plugin> running = Maps.newConcurrentMap();
+ private final ConcurrentMap<String, Plugin> disabled = Maps.newConcurrentMap();
+ private final Map<String, FileSnapshot> broken = Maps.newHashMap();
+ private final Map<Plugin, CleanupHandle> cleanupHandles = Maps.newConcurrentMap();
+ private final Queue<Plugin> toCleanup = new ArrayDeque<>();
private final Provider<PluginCleanerTask> cleaner;
private final PluginScannerThread scanner;
private final Provider<String> urlProvider;
@@ -108,11 +107,6 @@
env = pe;
srvInfoImpl = sii;
pluginUserFactory = puf;
- running = Maps.newConcurrentMap();
- disabled = Maps.newConcurrentMap();
- broken = new HashMap<>();
- toCleanup = new ArrayDeque<>();
- cleanupHandles = Maps.newConcurrentMap();
cleaner = pct;
urlProvider = provider;
persistentCacheFactory = cacheFactory;
@@ -207,7 +201,7 @@
}
private synchronized void unloadPlugin(Plugin plugin) {
- persistentCacheFactory.onStop(plugin);
+ persistentCacheFactory.onStop(plugin.getName());
String name = plugin.getName();
log.info(String.format("Unloading plugin %s, version %s", name, plugin.getVersion()));
plugin.stop(env);
@@ -495,7 +489,12 @@
unloadPlugin(oldPlugin);
}
if (!newPlugin.isDisabled()) {
- newPlugin.start(env);
+ try {
+ newPlugin.start(env);
+ } catch (Throwable e) {
+ newPlugin.stop(env);
+ throw e;
+ }
}
if (reload) {
env.onReloadPlugin(oldPlugin, newPlugin);
diff --git a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
index 3b75256..61a7ef2 100644
--- a/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
+++ b/java/com/google/gerrit/server/project/BooleanProjectConfigTransformations.java
@@ -71,7 +71,8 @@
.build();
static {
- // Verify that each BooleanProjectConfig has to/from API mappers in BooleanProjectConfigTransformations
+ // Verify that each BooleanProjectConfig has to/from API mappers in
+ // BooleanProjectConfigTransformations
if (!Sets.symmetricDifference(
MAPPER.keySet(), new HashSet<>(Arrays.asList(BooleanProjectConfig.values())))
.isEmpty()) {
diff --git a/java/com/google/gerrit/server/project/CommentLinkProvider.java b/java/com/google/gerrit/server/project/CommentLinkProvider.java
index bc70232..516965b 100644
--- a/java/com/google/gerrit/server/project/CommentLinkProvider.java
+++ b/java/com/google/gerrit/server/project/CommentLinkProvider.java
@@ -17,27 +17,31 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
+import com.google.gerrit.server.config.ConfigUpdatedEvent;
+import com.google.gerrit.server.config.GerritConfigListener;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public class CommentLinkProvider implements Provider<List<CommentLinkInfo>> {
+@Singleton
+public class CommentLinkProvider implements Provider<List<CommentLinkInfo>>, GerritConfigListener {
private static final Logger log = LoggerFactory.getLogger(CommentLinkProvider.class);
- private final Config cfg;
+ private volatile List<CommentLinkInfo> commentLinks;
@Inject
CommentLinkProvider(@GerritServerConfig Config cfg) {
- this.cfg = cfg;
+ this.commentLinks = parseConfig(cfg);
}
- @Override
- public List<CommentLinkInfo> get() {
+ private List<CommentLinkInfo> parseConfig(Config cfg) {
Set<String> subsections = cfg.getSubsections(ProjectConfig.COMMENTLINK);
List<CommentLinkInfo> cls = Lists.newArrayListWithCapacity(subsections.size());
for (String name : subsections) {
@@ -54,4 +58,18 @@
}
return ImmutableList.copyOf(cls);
}
+
+ @Override
+ public List<CommentLinkInfo> get() {
+ return commentLinks;
+ }
+
+ @Override
+ public List<ConfigUpdatedEvent.Update> configUpdated(ConfigUpdatedEvent event) {
+ if (event.isSectionUpdated(ProjectConfig.COMMENTLINK)) {
+ commentLinks = parseConfig(event.getNewConfig());
+ return Collections.singletonList(event.accept(ProjectConfig.COMMENTLINK));
+ }
+ return Collections.emptyList();
+ }
}
diff --git a/java/com/google/gerrit/server/project/ProjectCache.java b/java/com/google/gerrit/server/project/ProjectCache.java
index 9ebcc99..c7858dd 100644
--- a/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/java/com/google/gerrit/server/project/ProjectCache.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.project;
import com.google.common.collect.ImmutableSortedSet;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import java.io.IOException;
@@ -32,19 +33,31 @@
* Get the cached data for a project by its unique name.
*
* @param projectName name of the project.
- * @return the cached data; null if no such project exists or a error occurred.
+ * @return the cached data; null if no such project exists, projectName is null or an error
+ * occurred.
* @see #checkedGet(com.google.gerrit.reviewdb.client.Project.NameKey)
*/
- ProjectState get(Project.NameKey projectName);
+ ProjectState get(@Nullable Project.NameKey projectName);
/**
* Get the cached data for a project by its unique name.
*
* @param projectName name of the project.
* @throws IOException when there was an error.
- * @return the cached data; null if no such project exists.
+ * @return the cached data; null if no such project exists or projectName is null.
*/
- ProjectState checkedGet(Project.NameKey projectName) throws IOException;
+ ProjectState checkedGet(@Nullable Project.NameKey projectName) throws IOException;
+
+ /**
+ * Get the cached data for a project by its unique name.
+ *
+ * @param projectName name of the project.
+ * @param strict true when any error generates an exception
+ * @throws Exception in case of any error (strict = true) or only for I/O or other internal
+ * errors.
+ * @return the cached data or null when strict = false
+ */
+ public ProjectState checkedGet(Project.NameKey projectName, boolean strict) throws Exception;
/**
* Invalidate the cached information about the given project, and triggers reindexing for it
diff --git a/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index f9373ac..4975c55 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -142,24 +142,33 @@
return null;
}
try {
- ProjectState state = byName.get(projectName.get());
- if (state != null && state.needsRefresh(clock.read())) {
- byName.invalidate(projectName.get());
- state = byName.get(projectName.get());
- }
- return state;
- } catch (ExecutionException e) {
+ return strictCheckedGet(projectName);
+ } catch (Exception e) {
if (!(e.getCause() instanceof RepositoryNotFoundException)) {
log.warn(String.format("Cannot read project %s", projectName.get()), e);
Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
throw new IOException(e);
}
- log.warn("Cannot find project {}", projectName.get(), e);
+ log.debug("Cannot find project {}", projectName.get(), e);
return null;
}
}
@Override
+ public ProjectState checkedGet(Project.NameKey projectName, boolean strict) throws Exception {
+ return strict ? strictCheckedGet(projectName) : checkedGet(projectName);
+ }
+
+ private ProjectState strictCheckedGet(Project.NameKey projectName) throws Exception {
+ ProjectState state = byName.get(projectName.get());
+ if (state != null && state.needsRefresh(clock.read())) {
+ byName.invalidate(projectName.get());
+ state = byName.get(projectName.get());
+ }
+ return state;
+ }
+
+ @Override
public void evict(Project p) throws IOException {
evict(p.getNameKey());
}
diff --git a/java/com/google/gerrit/server/project/ProjectCacheWarmer.java b/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
index 66bbcca..10ab746 100644
--- a/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
+++ b/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
@@ -20,8 +20,6 @@
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -50,25 +48,25 @@
new ScheduledThreadPoolExecutor(
config.getInt("cache", "projects", "loadThreads", cpus),
new ThreadFactoryBuilder().setNameFormat("ProjectCacheLoader-%d").build());
- ExecutorService scheduler = Executors.newFixedThreadPool(1);
+ Thread scheduler =
+ new Thread(
+ () -> {
+ for (Project.NameKey name : cache.all()) {
+ pool.execute(() -> cache.get(name));
+ }
+ pool.shutdown();
+ try {
+ pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+ log.info("Finished loading project cache");
+ } catch (InterruptedException e) {
+ log.warn("Interrupted while waiting for project cache to load");
+ }
+ });
+ scheduler.setName("ProjectCacheWarmer");
+ scheduler.setDaemon(true);
log.info("Loading project cache");
- scheduler.execute(
- () -> {
- for (Project.NameKey name : cache.all()) {
- pool.execute(
- () -> {
- cache.get(name);
- });
- }
- pool.shutdown();
- try {
- pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
- log.info("Finished loading project cache");
- } catch (InterruptedException e) {
- log.warn("Interrupted while waiting for project cache to load");
- }
- });
+ scheduler.start();
}
}
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 15bc54c..b9e0266 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -81,6 +81,21 @@
public class ProjectConfig extends VersionedMetaData implements ValidationError.Sink {
public static final String COMMENTLINK = "commentlink";
+ public static final String LABEL = "label";
+ public static final String KEY_FUNCTION = "function";
+ public static final String KEY_DEFAULT_VALUE = "defaultValue";
+ public static final String KEY_COPY_MIN_SCORE = "copyMinScore";
+ public static final String KEY_ALLOW_POST_SUBMIT = "allowPostSubmit";
+ public static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
+ public static final String KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE =
+ "copyAllScoresOnMergeFirstParentUpdate";
+ public static final String KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE = "copyAllScoresOnTrivialRebase";
+ public static final String KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = "copyAllScoresIfNoCodeChange";
+ public static final String KEY_COPY_ALL_SCORES_IF_NO_CHANGE = "copyAllScoresIfNoChange";
+ public static final String KEY_VALUE = "value";
+ public static final String KEY_CAN_OVERRIDE = "canOverride";
+ public static final String KEY_BRANCH = "branch";
+
private static final String KEY_MATCH = "match";
private static final String KEY_HTML = "html";
private static final String KEY_LINK = "link";
@@ -131,22 +146,6 @@
private static final String KEY_DEFAULT = "default";
private static final String KEY_LOCAL_DEFAULT = "local-default";
- private static final String LABEL = "label";
- private static final String KEY_FUNCTION = "function";
- private static final String KEY_DEFAULT_VALUE = "defaultValue";
- private static final String KEY_COPY_MIN_SCORE = "copyMinScore";
- private static final String KEY_ALLOW_POST_SUBMIT = "allowPostSubmit";
- private static final String KEY_COPY_MAX_SCORE = "copyMaxScore";
- private static final String KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE =
- "copyAllScoresOnMergeFirstParentUpdate";
- private static final String KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE =
- "copyAllScoresOnTrivialRebase";
- private static final String KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE = "copyAllScoresIfNoCodeChange";
- private static final String KEY_COPY_ALL_SCORES_IF_NO_CHANGE = "copyAllScoresIfNoChange";
- private static final String KEY_VALUE = "value";
- private static final String KEY_CAN_OVERRIDE = "canOverride";
- private static final String KEY_BRANCH = "branch";
-
private static final String LEGACY_PERMISSION_PUSH_TAG = "pushTag";
private static final String LEGACY_PERMISSION_PUSH_SIGNED_TAG = "pushSignedTag";
diff --git a/java/com/google/gerrit/server/project/ProjectState.java b/java/com/google/gerrit/server/project/ProjectState.java
index 11327cd..e064265 100644
--- a/java/com/google/gerrit/server/project/ProjectState.java
+++ b/java/com/google/gerrit/server/project/ProjectState.java
@@ -261,7 +261,7 @@
ProjectLevelConfig cfg = new ProjectLevelConfig(fileName, this);
try (Repository git = gitMgr.openRepository(getNameKey())) {
- cfg.load(git);
+ cfg.load(git, config.getRevision());
} catch (IOException | ConfigInvalidException e) {
log.warn("Failed to load " + fileName + " for " + getName(), e);
}
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index f627ec8..ce236dc 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -49,6 +49,7 @@
private static final Logger log = LoggerFactory.getLogger(AccountQueryBuilder.class);
public static final String FIELD_ACCOUNT = "account";
+ public static final String FIELD_CAN_SEE = "cansee";
public static final String FIELD_EMAIL = "email";
public static final String FIELD_LIMIT = "limit";
public static final String FIELD_NAME = "name";
diff --git a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
index b5e7b90..b008092 100644
--- a/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
+++ b/java/com/google/gerrit/server/query/account/CanSeeChangePredicate.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.query.account;
import com.google.gerrit.index.query.PostFilterPredicate;
-import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -24,8 +23,6 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
-import java.util.Collection;
-import java.util.Objects;
public class CanSeeChangePredicate extends PostFilterPredicate<AccountState> {
private final Provider<ReviewDb> db;
@@ -34,6 +31,7 @@
CanSeeChangePredicate(
Provider<ReviewDb> db, PermissionBackend permissionBackend, ChangeNotes changeNotes) {
+ super(AccountQueryBuilder.FIELD_CAN_SEE, changeNotes.getChangeId().toString());
this.db = db;
this.permissionBackend = permissionBackend;
this.changeNotes = changeNotes;
@@ -56,24 +54,4 @@
public int getCost() {
return 1;
}
-
- @Override
- public Predicate<AccountState> copy(Collection<? extends Predicate<AccountState>> children) {
- return new CanSeeChangePredicate(db, permissionBackend, changeNotes);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(changeNotes.getChange().getChangeId());
- }
-
- @Override
- public boolean equals(Object other) {
- if (other == null) {
- return false;
- }
- return getClass() == other.getClass()
- && changeNotes.getChange().getChangeId()
- == ((CanSeeChangePredicate) other).changeNotes.getChange().getChangeId();
- }
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index fccb14a..c877544 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -451,6 +451,13 @@
this.notes = notes;
}
+ /**
+ * If false, omit fields that require database/repo IO.
+ *
+ * <p>This is used to enforce that the dashboard is rendered from the index only. If {@code
+ * lazyLoad} is on, the {@code ChangeData} object will load from the database ("lazily") when a
+ * field accessor is called.
+ */
public ChangeData setLazyLoad(boolean load) {
lazyLoad = load;
return this;
diff --git a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index 1052d33..5d5a95f 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -23,12 +23,12 @@
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import java.io.IOException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -89,11 +89,13 @@
.database(db)
.test(ChangePermission.READ);
} catch (PermissionBackendException e) {
- if (e.getCause() instanceof NoSuchProjectException) {
- logger.info("No such project: {}", cd.project());
+ Throwable cause = e.getCause();
+ if (cause instanceof RepositoryNotFoundException) {
+ logger.warn(
+ "Skipping change {} because the corresponding repository was not found", cd.getId(), e);
return false;
}
- throw new OrmException("unable to check permissions", e);
+ throw new OrmException("unable to check permissions on change " + cd.getId(), e);
}
if (visible) {
cd.cacheVisibleTo(user);
diff --git a/java/com/google/gerrit/server/query/change/ChangeOperatorPredicate.java b/java/com/google/gerrit/server/query/change/ChangeOperatorPredicate.java
deleted file mode 100644
index 8b08536..0000000
--- a/java/com/google/gerrit/server/query/change/ChangeOperatorPredicate.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.index.query.Matchable;
-import com.google.gerrit.index.query.OperatorPredicate;
-
-public abstract class ChangeOperatorPredicate extends OperatorPredicate<ChangeData>
- implements Matchable<ChangeData> {
-
- protected ChangeOperatorPredicate(String name, String value) {
- super(name, value);
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 5a04a7c..630f2f3 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -1103,7 +1103,7 @@
VersionedAccountDestinations d = VersionedAccountDestinations.forUser(self());
d.load(git);
Set<Branch.NameKey> destinations = d.getDestinationList().getDestinations(name);
- if (destinations != null) {
+ if (destinations != null && !destinations.isEmpty()) {
return new DestinationPredicate(destinations, name);
}
} catch (RepositoryNotFoundException e) {
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index e853cc0..f870951 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.query.change;
import com.google.gerrit.common.data.SubmitTypeRecord;
+import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
@@ -77,18 +78,17 @@
and.add(Predicate.or(filePredicates));
ChangeDataCache changeDataCache = new ChangeDataCache(cd, args.projectCache);
- and.add(new CheckConflict(ChangeQueryBuilder.FIELD_CONFLICTS, value, args, c, changeDataCache));
+ and.add(new CheckConflict(value, args, c, changeDataCache));
return Predicate.and(and);
}
- private static final class CheckConflict extends ChangeOperatorPredicate {
+ private static final class CheckConflict extends PostFilterPredicate<ChangeData> {
private final Arguments args;
private final Branch.NameKey dest;
private final ChangeDataCache changeDataCache;
- CheckConflict(
- String field, String value, Arguments args, Change c, ChangeDataCache changeDataCache) {
- super(field, value);
+ CheckConflict(String value, Arguments args, Change c, ChangeDataCache changeDataCache) {
+ super(ChangeQueryBuilder.FIELD_CONFLICTS, value);
this.args = args;
this.dest = c.getDest();
this.changeDataCache = changeDataCache;
diff --git a/java/com/google/gerrit/server/query/change/DestinationPredicate.java b/java/com/google/gerrit/server/query/change/DestinationPredicate.java
index 7f969e1..a824a87 100644
--- a/java/com/google/gerrit/server/query/change/DestinationPredicate.java
+++ b/java/com/google/gerrit/server/query/change/DestinationPredicate.java
@@ -14,12 +14,13 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwtorm.server.OrmException;
import java.util.Set;
-public class DestinationPredicate extends ChangeOperatorPredicate {
+public class DestinationPredicate extends PostFilterPredicate<ChangeData> {
protected Set<Branch.NameKey> destinations;
public DestinationPredicate(Set<Branch.NameKey> destinations, String value) {
diff --git a/java/com/google/gerrit/server/query/change/OwnerinPredicate.java b/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
index fec7f26..c48bdd5 100644
--- a/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
+++ b/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
@@ -14,25 +14,22 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gwtorm.server.OrmException;
-public class OwnerinPredicate extends ChangeOperatorPredicate {
+public class OwnerinPredicate extends PostFilterPredicate<ChangeData> {
protected final IdentifiedUser.GenericFactory userFactory;
protected final AccountGroup.UUID uuid;
public OwnerinPredicate(IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) {
- super(ChangeQueryBuilder.FIELD_OWNERIN, uuid.toString());
+ super(ChangeQueryBuilder.FIELD_OWNERIN, uuid.get());
this.userFactory = userFactory;
this.uuid = uuid;
}
- protected AccountGroup.UUID getAccountGroupUUID() {
- return uuid;
- }
-
@Override
public boolean match(ChangeData object) throws OrmException {
final Change change = object.change();
diff --git a/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
index 11f9d89..1894b06 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
@@ -14,17 +14,18 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gwtorm.server.OrmException;
-public class ReviewerinPredicate extends ChangeOperatorPredicate {
+public class ReviewerinPredicate extends PostFilterPredicate<ChangeData> {
protected final IdentifiedUser.GenericFactory userFactory;
protected final AccountGroup.UUID uuid;
public ReviewerinPredicate(IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) {
- super(ChangeQueryBuilder.FIELD_REVIEWERIN, uuid.toString());
+ super(ChangeQueryBuilder.FIELD_REVIEWERIN, uuid.get());
this.userFactory = userFactory;
this.uuid = uuid;
}
diff --git a/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index a084b35..a49e8c5 100644
--- a/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -36,4 +36,9 @@
public GroupMembership getEffectiveGroups() {
return groups;
}
+
+ @Override
+ public Object getCacheKey() {
+ return groups.getKnownGroups();
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/BUILD b/java/com/google/gerrit/server/restapi/BUILD
index a307d43..6c2a50d 100644
--- a/java/com/google/gerrit/server/restapi/BUILD
+++ b/java/com/google/gerrit/server/restapi/BUILD
@@ -25,6 +25,7 @@
"//lib:gwtorm",
"//lib:servlet-api-3_1",
"//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/commons:codec",
"//lib/commons:compress",
"//lib/commons:lang",
diff --git a/java/com/google/gerrit/server/restapi/account/Capabilities.java b/java/com/google/gerrit/server/restapi/account/Capabilities.java
index 2dd54a5..8047b26 100644
--- a/java/com/google/gerrit/server/restapi/account/Capabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/Capabilities.java
@@ -14,6 +14,9 @@
package com.google.gerrit.server.restapi.account;
+import static com.google.gerrit.server.permissions.DefaultPermissionMappings.globalOrPluginPermissionName;
+import static com.google.gerrit.server.permissions.DefaultPermissionMappings.globalPermission;
+
import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
import com.google.gerrit.extensions.api.access.PluginPermission;
import com.google.gerrit.extensions.registration.DynamicMap;
@@ -32,6 +35,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.util.Optional;
@Singleton
class Capabilities implements ChildCollection<AccountResource, AccountResource.Capability> {
@@ -60,6 +64,7 @@
@Override
public Capability parse(AccountResource parent, IdString id)
throws ResourceNotFoundException, AuthException, PermissionBackendException {
+ permissionBackend.checkUsesDefaultCapabilities();
IdentifiedUser target = parent.getUser();
if (self.get() != target) {
permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
@@ -67,16 +72,16 @@
GlobalOrPluginPermission perm = parse(id);
if (permissionBackend.user(target).test(perm)) {
- return new AccountResource.Capability(target, perm.permissionName());
+ return new AccountResource.Capability(target, globalOrPluginPermissionName(perm));
}
throw new ResourceNotFoundException(id);
}
private GlobalOrPluginPermission parse(IdString id) throws ResourceNotFoundException {
String name = id.get();
- GlobalOrPluginPermission perm = GlobalPermission.byName(name);
- if (perm != null) {
- return perm;
+ Optional<GlobalPermission> perm = globalPermission(name);
+ if (perm.isPresent()) {
+ return perm.get();
}
int dash = name.lastIndexOf('-');
diff --git a/java/com/google/gerrit/server/restapi/account/CreateEmail.java b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
index 2248e35..57f7d4a 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateEmail.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateEmail.java
@@ -20,12 +20,11 @@
import com.google.gerrit.extensions.api.accounts.EmailInput;
import com.google.gerrit.extensions.client.AccountFieldName;
import com.google.gerrit.extensions.common.EmailInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
@@ -90,9 +89,8 @@
@Override
public Response<EmailInfo> apply(AccountResource rsrc, EmailInput input)
- throws AuthException, BadRequestException, ResourceConflictException,
- ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException,
- IOException, ConfigInvalidException, PermissionBackendException {
+ throws RestApiException, OrmException, EmailException, MethodNotAllowedException, IOException,
+ ConfigInvalidException, PermissionBackendException {
if (input == null) {
input = new EmailInput();
}
@@ -110,9 +108,8 @@
/** To be used from plugins that want to create emails without permission checks. */
public Response<EmailInfo> apply(IdentifiedUser user, EmailInput input)
- throws AuthException, BadRequestException, ResourceConflictException,
- ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException,
- IOException, ConfigInvalidException, PermissionBackendException {
+ throws RestApiException, OrmException, EmailException, MethodNotAllowedException, IOException,
+ ConfigInvalidException, PermissionBackendException {
if (input == null) {
input = new EmailInput();
}
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
index f278df2..998c6f0 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteSshKey.java
@@ -61,7 +61,7 @@
permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
}
- authorizedKeys.deleteKey(rsrc.getUser().getAccountId(), rsrc.getSshKey().getKey().get());
+ authorizedKeys.deleteKey(rsrc.getUser().getAccountId(), rsrc.getSshKey().seq());
rsrc.getUser().getUserName().ifPresent(sshKeyCache::evict);
return Response.none();
diff --git a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
index c623e3e..f38d367 100644
--- a/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/account/GetCapabilities.java
@@ -15,6 +15,9 @@
package com.google.gerrit.server.restapi.account;
import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
+import static com.google.gerrit.server.permissions.DefaultPermissionMappings.globalOrPluginPermissionName;
+import static com.google.gerrit.server.permissions.DefaultPermissionMappings.globalPermissionName;
+import static com.google.gerrit.server.permissions.DefaultPermissionMappings.pluginPermissionName;
import com.google.common.collect.Iterables;
import com.google.gerrit.common.data.GlobalCapability;
@@ -23,8 +26,9 @@
import com.google.gerrit.extensions.api.access.PluginPermission;
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.registration.DynamicMap;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.OptionUtil;
@@ -75,7 +79,8 @@
}
@Override
- public Object apply(AccountResource rsrc) throws AuthException, PermissionBackendException {
+ public Object apply(AccountResource rsrc) throws RestApiException, PermissionBackendException {
+ permissionBackend.checkUsesDefaultCapabilities();
PermissionBackend.WithUser perm = permissionBackend.currentUser();
if (self.get() != rsrc.getUser()) {
perm.check(GlobalPermission.ADMINISTRATE_SERVER);
@@ -84,7 +89,7 @@
Map<String, Object> have = new LinkedHashMap<>();
for (GlobalOrPluginPermission p : perm.test(permissionsToTest())) {
- have.put(p.permissionName(), true);
+ have.put(globalOrPluginPermissionName(p), true);
}
AccountLimits limits = limitsFactory.create(rsrc.getUser());
@@ -99,7 +104,7 @@
private Set<GlobalOrPluginPermission> permissionsToTest() {
Set<GlobalOrPluginPermission> toTest = new HashSet<>();
for (GlobalPermission p : GlobalPermission.values()) {
- if (want(p.permissionName())) {
+ if (want(globalPermissionName(p))) {
toTest.add(p);
}
}
@@ -107,7 +112,7 @@
for (String pluginName : pluginCapabilities.plugins()) {
for (String capability : pluginCapabilities.byPlugin(pluginName).keySet()) {
PluginPermission p = new PluginPermission(pluginName, capability);
- if (want(p.permissionName())) {
+ if (want(pluginPermissionName(p))) {
toTest.add(p);
}
}
@@ -158,8 +163,16 @@
@Singleton
static class CheckOne implements RestReadView<AccountResource.Capability> {
+ private final PermissionBackend permissionBackend;
+
+ @Inject
+ CheckOne(PermissionBackend permissionBackend) {
+ this.permissionBackend = permissionBackend;
+ }
+
@Override
- public BinaryResult apply(Capability resource) {
+ public BinaryResult apply(Capability resource) throws ResourceNotFoundException {
+ permissionBackend.checkUsesDefaultCapabilities();
return BinaryResult.create("ok\n");
}
}
diff --git a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
index cd8dc09..15ad75f 100644
--- a/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
+++ b/java/com/google/gerrit/server/restapi/account/GetSshKeys.java
@@ -70,12 +70,12 @@
public static SshKeyInfo newSshKeyInfo(AccountSshKey sshKey) {
SshKeyInfo info = new SshKeyInfo();
- info.seq = sshKey.getKey().get();
- info.sshPublicKey = sshKey.getSshPublicKey();
- info.encodedKey = sshKey.getEncodedKey();
- info.algorithm = sshKey.getAlgorithm();
- info.comment = Strings.emptyToNull(sshKey.getComment());
- info.valid = sshKey.isValid();
+ info.seq = sshKey.seq();
+ info.sshPublicKey = sshKey.sshPublicKey();
+ info.encodedKey = sshKey.encodedKey();
+ info.algorithm = sshKey.algorithm();
+ info.comment = Strings.emptyToNull(sshKey.comment());
+ info.valid = sshKey.valid();
return info;
}
}
diff --git a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
index 7f7c2ae..e33f906 100644
--- a/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
+++ b/java/com/google/gerrit/server/restapi/account/PutHttpPassword.java
@@ -39,6 +39,7 @@
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
+import java.util.Optional;
import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -102,10 +103,9 @@
ConfigInvalidException {
String userName =
user.getUserName().orElseThrow(() -> new ResourceConflictException("username must be set"));
- ExternalId extId = externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, userName));
- if (extId == null) {
- throw new ResourceNotFoundException();
- }
+ Optional<ExternalId> optionalExtId =
+ externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, userName));
+ ExternalId extId = optionalExtId.orElseThrow(() -> new ResourceNotFoundException());
accountsUpdateProvider
.get()
.update(
diff --git a/java/com/google/gerrit/server/restapi/account/PutPreferred.java b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
index a30e074..65285c3 100644
--- a/java/com/google/gerrit/server/restapi/account/PutPreferred.java
+++ b/java/com/google/gerrit/server/restapi/account/PutPreferred.java
@@ -14,16 +14,22 @@
package com.google.gerrit.server.restapi.account;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+
import com.google.gerrit.extensions.common.Input;
-import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -32,38 +38,49 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
@Singleton
public class PutPreferred implements RestModifyView<AccountResource.Email, Input> {
+ private static final Logger log = LoggerFactory.getLogger(PutPreferred.class);
private final Provider<CurrentUser> self;
private final PermissionBackend permissionBackend;
private final Provider<AccountsUpdate> accountsUpdateProvider;
+ private final ExternalIds externalIds;
@Inject
PutPreferred(
Provider<CurrentUser> self,
PermissionBackend permissionBackend,
- @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
+ @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider,
+ ExternalIds externalIds) {
this.self = self;
this.permissionBackend = permissionBackend;
this.accountsUpdateProvider = accountsUpdateProvider;
+ this.externalIds = externalIds;
}
@Override
public Response<String> apply(AccountResource.Email rsrc, Input input)
- throws AuthException, ResourceNotFoundException, OrmException, IOException,
- PermissionBackendException, ConfigInvalidException {
+ throws RestApiException, OrmException, IOException, PermissionBackendException,
+ ConfigInvalidException {
if (self.get() != rsrc.getUser()) {
permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
}
return apply(rsrc.getUser(), rsrc.getEmail());
}
- public Response<String> apply(IdentifiedUser user, String email)
- throws ResourceNotFoundException, IOException, ConfigInvalidException, OrmException {
+ public Response<String> apply(IdentifiedUser user, String preferredEmail)
+ throws RestApiException, IOException, ConfigInvalidException, OrmException {
+ AtomicReference<Optional<RestApiException>> exception = new AtomicReference<>(Optional.empty());
AtomicBoolean alreadyPreferred = new AtomicBoolean(false);
accountsUpdateProvider
.get()
@@ -71,13 +88,68 @@
"Set Preferred Email via API",
user.getAccountId(),
(a, u) -> {
- if (email.equals(a.getAccount().getPreferredEmail())) {
+ if (preferredEmail.equals(a.getAccount().getPreferredEmail())) {
alreadyPreferred.set(true);
} else {
- u.setPreferredEmail(email);
+ // check if the user has a matching email
+ String matchingEmail = null;
+ for (String email :
+ a.getExternalIds()
+ .stream()
+ .map(ExternalId::email)
+ .filter(Objects::nonNull)
+ .collect(toSet())) {
+ if (email.equals(preferredEmail)) {
+ // we have an email that matches exactly, prefer this one
+ matchingEmail = email;
+ break;
+ } else if (matchingEmail == null && email.equalsIgnoreCase(preferredEmail)) {
+ // we found an email that matches but has a different case
+ matchingEmail = email;
+ }
+ }
+
+ if (matchingEmail == null) {
+ // user doesn't have an external ID for this email
+ if (user.hasEmailAddress(preferredEmail)) {
+ // but Realm says the user is allowed to use this email
+ Set<ExternalId> existingExtIdsWithThisEmail =
+ externalIds.byEmail(preferredEmail);
+ if (!existingExtIdsWithThisEmail.isEmpty()) {
+ // but the email is already assigned to another account
+ log.warn(
+ "Cannot set preferred email {} for account {} because it is owned"
+ + " by the following account(s): {}",
+ preferredEmail,
+ user.getAccountId(),
+ existingExtIdsWithThisEmail
+ .stream()
+ .map(ExternalId::accountId)
+ .collect(toList()));
+ exception.set(
+ Optional.of(
+ new ResourceConflictException("email in use by another account")));
+ return;
+ }
+
+ // claim the email now
+ u.addExternalId(ExternalId.createEmail(a.getAccount().getId(), preferredEmail));
+ matchingEmail = preferredEmail;
+ } else {
+ // Realm says that the email doesn't belong to the user. This can only happen as
+ // a race condition because EmailsCollection would have thrown
+ // ResourceNotFoundException already before invoking this REST endpoint.
+ exception.set(Optional.of(new ResourceNotFoundException(preferredEmail)));
+ return;
+ }
+ }
+ u.setPreferredEmail(matchingEmail);
}
})
.orElseThrow(() -> new ResourceNotFoundException("account not found"));
+ if (exception.get().isPresent()) {
+ throw exception.get().get();
+ }
return alreadyPreferred.get() ? Response.ok("") : Response.created("");
}
}
diff --git a/java/com/google/gerrit/server/restapi/account/PutUsername.java b/java/com/google/gerrit/server/restapi/account/PutUsername.java
index 4024c10..073d724 100644
--- a/java/com/google/gerrit/server/restapi/account/PutUsername.java
+++ b/java/com/google/gerrit/server/restapi/account/PutUsername.java
@@ -42,6 +42,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
@@ -109,8 +110,8 @@
u -> u.addExternalId(ExternalId.create(key, accountId, null, null)));
} catch (OrmDuplicateKeyException dupeErr) {
// If we are using this identity, don't report the exception.
- ExternalId other = externalIds.get(key);
- if (other != null && other.accountId().equals(accountId)) {
+ Optional<ExternalId> other = externalIds.get(key);
+ if (other.isPresent() && other.get().accountId().equals(accountId)) {
return input.username;
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 5ea72c0..2ecd0b9 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -172,6 +172,7 @@
private final Config gerritConfig;
private final WorkInProgressOp.Factory workInProgressOpFactory;
private final ProjectCache projectCache;
+ private final boolean strictLabels;
@Inject
PostReview(
@@ -211,6 +212,7 @@
this.gerritConfig = gerritConfig;
this.workInProgressOpFactory = workInProgressOpFactory;
this.projectCache = projectCache;
+ this.strictLabels = gerritConfig.getBoolean("change", "strictLabels", false);
}
@Override
@@ -372,7 +374,10 @@
reviewerResult.gatherResults();
}
- emailReviewers(revision.getChange(), reviewerResults, reviewerNotify, accountsToNotify);
+ boolean readyForReview =
+ (output.ready != null && output.ready) || !revision.getChange().isWorkInProgress();
+ emailReviewers(
+ revision.getChange(), reviewerResults, reviewerNotify, accountsToNotify, readyForReview);
}
return Response.ok(output);
@@ -405,7 +410,8 @@
Change change,
List<PostReviewers.Addition> reviewerAdditions,
@Nullable NotifyHandling notify,
- ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+ ListMultimap<RecipientType, Account.Id> accountsToNotify,
+ boolean readyForReview) {
List<Account.Id> to = new ArrayList<>();
List<Account.Id> cc = new ArrayList<>();
List<Address> toByEmail = new ArrayList<>();
@@ -423,7 +429,8 @@
reviewerAdditions
.get(0)
.op
- .emailReviewers(change, to, cc, toByEmail, ccByEmail, notify, accountsToNotify);
+ .emailReviewers(
+ change, to, cc, toByEmail, ccByEmail, notify, accountsToNotify, readyForReview);
}
}
@@ -445,8 +452,12 @@
Map.Entry<String, Short> ent = itr.next();
LabelType type = labelTypes.byLabel(ent.getKey());
if (type == null) {
- throw new BadRequestException(
- String.format("label \"%s\" is not a configured label", ent.getKey()));
+ if (strictLabels) {
+ throw new BadRequestException(
+ String.format("label \"%s\" is not a configured label", ent.getKey()));
+ }
+ itr.remove();
+ continue;
}
if (!caller.isInternalUser()) {
@@ -485,8 +496,12 @@
Map.Entry<String, Short> ent = itr.next();
LabelType lt = labelTypes.byLabel(ent.getKey());
if (lt == null) {
- throw new BadRequestException(
- String.format("label \"%s\" is not a configured label", ent.getKey()));
+ if (strictLabels) {
+ throw new BadRequestException(
+ String.format("label \"%s\" is not a configured label", ent.getKey()));
+ }
+ itr.remove();
+ continue;
}
if (ent.getValue() == null || ent.getValue() == 0) {
@@ -496,8 +511,12 @@
}
if (lt.getValue(ent.getValue()) == null) {
- throw new BadRequestException(
- String.format("label \"%s\": %d is not a valid value", ent.getKey(), ent.getValue()));
+ if (strictLabels) {
+ throw new BadRequestException(
+ String.format("label \"%s\": %d is not a valid value", ent.getKey(), ent.getValue()));
+ }
+ itr.remove();
+ continue;
}
short val = ent.getValue();
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index 2049929..46955e8 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -51,7 +51,6 @@
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyUtil;
-import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.group.SystemGroupBackend;
@@ -92,7 +91,6 @@
public static final int DEFAULT_MAX_REVIEWERS = 20;
private final AccountsCollection accounts;
- private final ReviewerResource.Factory reviewerFactory;
private final PermissionBackend permissionBackend;
private final GroupsCollection groupsCollection;
@@ -113,7 +111,6 @@
@Inject
PostReviewers(
AccountsCollection accounts,
- ReviewerResource.Factory reviewerFactory,
PermissionBackend permissionBackend,
GroupsCollection groupsCollection,
GroupMembers groupMembers,
@@ -132,7 +129,6 @@
OutgoingEmailValidator validator) {
super(retryHelper);
this.accounts = accounts;
- this.reviewerFactory = reviewerFactory;
this.permissionBackend = permissionBackend;
this.groupsCollection = groupsCollection;
this.groupMembers = groupMembers;
@@ -237,12 +233,12 @@
boolean allowGroup,
boolean allowByEmail)
throws OrmException, PermissionBackendException, IOException, ConfigInvalidException {
- IdentifiedUser user = null;
+ IdentifiedUser reviewerUser = null;
boolean exactMatchFound = false;
try {
- user = accounts.parse(reviewer);
- if (reviewer.equalsIgnoreCase(user.getName())
- || reviewer.equals(String.valueOf(user.getAccountId()))) {
+ reviewerUser = accounts.parse(reviewer);
+ if (reviewer.equalsIgnoreCase(reviewerUser.getName())
+ || reviewer.equals(String.valueOf(reviewerUser.getAccountId()))) {
exactMatchFound = true;
}
} catch (UnprocessableEntityException | AuthException e) {
@@ -255,22 +251,20 @@
return null;
}
- ReviewerResource rrsrc = reviewerFactory.create(rsrc, user.getAccountId());
- Account member = rrsrc.getReviewerUser().getAccount();
PermissionBackend.ForRef perm =
- permissionBackend.user(rrsrc.getReviewerUser()).ref(rrsrc.getChange().getDest());
- if (isValidReviewer(member, perm)) {
+ permissionBackend.absentUser(reviewerUser.getAccountId()).ref(rsrc.getChange().getDest());
+ if (isValidReviewer(reviewerUser.getAccount(), perm)) {
return new Addition(
reviewer,
rsrc,
- ImmutableSet.of(member.getId()),
+ ImmutableSet.of(reviewerUser.getAccountId()),
null,
state,
notify,
accountsToNotify,
exactMatchFound);
}
- if (!member.isActive()) {
+ if (!reviewerUser.getAccount().isActive()) {
if (allowByEmail && state == CC) {
return null;
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java b/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java
index 79e8507..665040d 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewersOp.java
@@ -202,7 +202,8 @@
reviewersByEmail,
addedCCsByEmail,
notify,
- accountsToNotify);
+ accountsToNotify,
+ !rsrc.getChange().isWorkInProgress());
if (!addedReviewers.isEmpty()) {
List<AccountState> reviewers =
addedReviewers
@@ -221,7 +222,8 @@
Collection<Address> addedByEmail,
Collection<Address> copiedByEmail,
NotifyHandling notify,
- ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+ ListMultimap<RecipientType, Account.Id> accountsToNotify,
+ boolean readyForReview) {
if (added.isEmpty() && copied.isEmpty() && addedByEmail.isEmpty() && copiedByEmail.isEmpty()) {
return;
}
@@ -250,7 +252,7 @@
AddReviewerSender cm = addReviewerSenderFactory.create(change.getProject(), change.getId());
// Default to silent operation on WIP changes.
NotifyHandling defaultNotifyHandling =
- change.isWorkInProgress() ? NotifyHandling.NONE : NotifyHandling.ALL;
+ readyForReview ? NotifyHandling.ALL : NotifyHandling.NONE;
cm.setNotify(MoreObjects.firstNonNull(notify, defaultNotifyHandling));
cm.setAccountsToNotify(accountsToNotify);
cm.setFrom(userId);
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index 78687cd..dae37d6 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -29,10 +29,10 @@
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.server.change.ReviewerSuggestion;
import com.google.gerrit.server.change.SuggestedReviewer;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.ProjectState;
@@ -54,6 +54,7 @@
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@@ -78,7 +79,7 @@
private final Config config;
private final DynamicMap<ReviewerSuggestion> reviewerSuggestionPluginMap;
private final Provider<InternalChangeQuery> queryProvider;
- private final WorkQueue workQueue;
+ private final ExecutorService executor;
private final Provider<ReviewDb> dbProvider;
private final ApprovalsUtil approvalsUtil;
@@ -87,7 +88,7 @@
ChangeQueryBuilder changeQueryBuilder,
DynamicMap<ReviewerSuggestion> reviewerSuggestionPluginMap,
Provider<InternalChangeQuery> queryProvider,
- WorkQueue workQueue,
+ @FanOutExecutor ExecutorService executor,
Provider<ReviewDb> dbProvider,
ApprovalsUtil approvalsUtil,
@GerritServerConfig Config config) {
@@ -95,7 +96,7 @@
this.config = config;
this.queryProvider = queryProvider;
this.reviewerSuggestionPluginMap = reviewerSuggestionPluginMap;
- this.workQueue = workQueue;
+ this.executor = executor;
this.dbProvider = dbProvider;
this.approvalsUtil = approvalsUtil;
}
@@ -150,7 +151,7 @@
try {
List<Future<Set<SuggestedReviewer>>> futures =
- workQueue.getDefaultQueue().invokeAll(tasks, PLUGIN_QUERY_TIMEOUT, TimeUnit.MILLISECONDS);
+ executor.invokeAll(tasks, PLUGIN_QUERY_TIMEOUT, TimeUnit.MILLISECONDS);
Iterator<Double> weightIterator = weights.iterator();
for (Future<Set<SuggestedReviewer>> f : futures) {
double weight = weightIterator.next();
diff --git a/java/com/google/gerrit/server/restapi/change/Revisions.java b/java/com/google/gerrit/server/restapi/change/Revisions.java
index 9cf52b3..557d77a 100644
--- a/java/com/google/gerrit/server/restapi/change/Revisions.java
+++ b/java/com/google/gerrit/server/restapi/change/Revisions.java
@@ -87,7 +87,7 @@
if (id.get().equals("current")) {
PatchSet ps = psUtil.current(dbProvider.get(), change.getNotes());
if (ps != null && visible(change)) {
- return new RevisionResource(change, ps).doNotCache();
+ return RevisionResource.createNonCachable(change, ps);
}
throw new ResourceNotFoundException(id);
}
diff --git a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
index aff1979..bbfe75d 100644
--- a/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
+++ b/java/com/google/gerrit/server/restapi/change/SubmittedTogether.java
@@ -15,6 +15,8 @@
package com.google.gerrit.server.restapi.change;
import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
+import static java.util.Collections.reverseOrder;
+import static java.util.stream.Collectors.toList;
import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
import com.google.gerrit.extensions.api.changes.SubmittedTogetherOption;
@@ -41,6 +43,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
import org.kohsuke.args4j.Option;
@@ -56,6 +59,9 @@
private final EnumSet<ListChangesOption> jsonOpt =
EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.SUBMITTABLE);
+ private static final Comparator<ChangeData> COMPARATOR =
+ Comparator.comparing(ChangeData::project).thenComparing(cd -> cd.getId().id, reverseOrder());
+
private final ChangeJson.Factory json;
private final Provider<ReviewDb> dbProvider;
private final Provider<InternalChangeQuery> queryProvider;
@@ -129,7 +135,7 @@
if (c.getStatus().isOpen()) {
ChangeSet cs =
mergeSuperSet.get().completeChangeSet(dbProvider.get(), c, resource.getUser());
- cds = cs.changes().asList();
+ cds = ensureRequiredDataIsLoaded(cs.changes().asList());
hidden = cs.nonVisibleChanges().size();
} else if (c.getStatus().asChangeStatus() == ChangeStatus.MERGED) {
cds = queryProvider.get().bySubmissionId(c.getSubmissionId());
@@ -143,14 +149,7 @@
throw new AuthException("change would be submitted with a change that you cannot see");
}
- if (cds.size() <= 1 && hidden == 0) {
- cds = Collections.emptyList();
- } else {
- // Skip sorting for singleton lists, to avoid WalkSorter opening the
- // repo just to fill out the commit field in PatchSetData.
- cds = sort(cds);
- }
-
+ cds = sort(cds, hidden);
SubmittedTogetherInfo info = new SubmittedTogetherInfo();
info.changes = json.create(jsonOpt).lazyLoad(lazyLoad).formatChangeDatas(cds);
info.nonVisibleChanges = hidden;
@@ -161,11 +160,42 @@
}
}
- private List<ChangeData> sort(List<ChangeData> cds) throws OrmException, IOException {
+ private List<ChangeData> sort(List<ChangeData> cds, int hidden) throws OrmException, IOException {
+ if (cds.size() <= 1 && hidden == 0) {
+ // Skip sorting for singleton lists, to avoid WalkSorter opening the
+ // repo just to fill out the commit field in PatchSetData.
+ return Collections.emptyList();
+ }
+
+ long numProjectsDistinct = cds.stream().map(ChangeData::project).distinct().count();
+ long numProjects = cds.stream().map(ChangeData::project).count();
+
+ if (numProjects == numProjectsDistinct || numProjectsDistinct > 5) {
+ // We either have only a single change per project which means that WalkSorter won't make a
+ // difference compared to our index-backed sort, or we are looking at more than 5 projects
+ // which would make WalkSorter too expensive for this call.
+ return cds.stream().sorted(COMPARATOR).collect(toList());
+ }
+
+ // Perform more expensive walk-sort.
List<ChangeData> sorted = new ArrayList<>(cds.size());
for (PatchSetData psd : sorter.get().sort(cds)) {
sorted.add(psd.data());
}
return sorted;
}
+
+ private static List<ChangeData> ensureRequiredDataIsLoaded(List<ChangeData> cds)
+ throws OrmException {
+ // TODO(hiesel): Instead of calling these manually, either implement a helper that brings a
+ // database-backed change on-par with an index-backed change in terms of the populated fields in
+ // ChangeData or check if any of the ChangeDatas was loaded from the database and allow
+ // lazyloading if so.
+ for (ChangeData cd : cds) {
+ cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_LENIENT);
+ cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_STRICT);
+ cd.currentPatchSet();
+ }
+ return cds;
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
index dcb35ab..d32abe8 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestReviewers.java
@@ -14,9 +14,13 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.gerrit.server.config.GerritConfigListenerHelper.acceptIfChanged;
+
import com.google.gerrit.extensions.common.AccountVisibility;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.ConfigKey;
+import com.google.gerrit.server.config.GerritConfigListener;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -104,4 +108,12 @@
"maxWithoutConfirmation",
PostReviewers.DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
}
+
+ public static GerritConfigListener configListener() {
+ return acceptIfChanged(
+ ConfigKey.create("suggest", "maxSuggestedReviewers"),
+ ConfigKey.create("suggest", "accounts"),
+ ConfigKey.create("addreviewer", "maxAllowed"),
+ ConfigKey.create("addreviewer", "maxWithoutConfirmation"));
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/config/ConfigRestModule.java b/java/com/google/gerrit/server/restapi/config/ConfigRestModule.java
index 90fa5c4..0b94d16 100644
--- a/java/com/google/gerrit/server/restapi/config/ConfigRestModule.java
+++ b/java/com/google/gerrit/server/restapi/config/ConfigRestModule.java
@@ -37,6 +37,7 @@
get(CONFIG_KIND, "version").to(GetVersion.class);
get(CONFIG_KIND, "info").to(GetServerInfo.class);
post(CONFIG_KIND, "check.consistency").to(CheckConsistency.class);
+ post(CONFIG_KIND, "reload").to(ReloadConfig.class);
get(CONFIG_KIND, "preferences").to(GetPreferences.class);
put(CONFIG_KIND, "preferences").to(SetPreferences.class);
get(CONFIG_KIND, "preferences.diff").to(GetDiffPreferences.class);
diff --git a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
index 853e156..412b88d 100644
--- a/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
+++ b/java/com/google/gerrit/server/restapi/config/ListCapabilities.java
@@ -18,9 +18,11 @@
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.config.CapabilityConstants;
import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -36,16 +38,20 @@
private static final Logger log = LoggerFactory.getLogger(ListCapabilities.class);
private static final Pattern PLUGIN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9-]+$");
+ private final PermissionBackend permissionBackend;
private final DynamicMap<CapabilityDefinition> pluginCapabilities;
@Inject
- public ListCapabilities(DynamicMap<CapabilityDefinition> pluginCapabilities) {
+ public ListCapabilities(
+ PermissionBackend permissionBackend, DynamicMap<CapabilityDefinition> pluginCapabilities) {
+ this.permissionBackend = permissionBackend;
this.pluginCapabilities = pluginCapabilities;
}
@Override
public Map<String, CapabilityInfo> apply(ConfigResource resource)
- throws IllegalAccessException, NoSuchFieldException {
+ throws ResourceNotFoundException, IllegalAccessException, NoSuchFieldException {
+ permissionBackend.checkUsesDefaultCapabilities();
return ImmutableMap.<String, CapabilityInfo>builder()
.putAll(collectCoreCapabilities())
.putAll(collectPluginCapabilities())
diff --git a/java/com/google/gerrit/server/restapi/config/ReloadConfig.java b/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
new file mode 100644
index 0000000..0c9cf17
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/config/ReloadConfig.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.config;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.gerrit.extensions.api.config.ConfigUpdateEntryInfo;
+import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.config.ConfigUpdatedEvent;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.ConfigUpdateEntry;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
+import com.google.gerrit.server.config.GerritServerConfigReloader;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ReloadConfig implements RestModifyView<ConfigResource, Input> {
+
+ private GerritServerConfigReloader config;
+ private PermissionBackend permissions;
+
+ @Inject
+ ReloadConfig(GerritServerConfigReloader config, PermissionBackend permissions) {
+ this.config = config;
+ this.permissions = permissions;
+ }
+
+ @Override
+ public Map<String, List<ConfigUpdateEntryInfo>> apply(ConfigResource resource, Input input)
+ throws RestApiException, PermissionBackendException {
+ permissions.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
+
+ List<ConfigUpdatedEvent.Update> updates = config.reloadConfig();
+
+ Map<String, List<ConfigUpdateEntryInfo>> reply = new HashMap<>();
+ for (UpdateResult result : UpdateResult.values()) {
+ reply.put(result.name().toLowerCase(), new ArrayList<>());
+ }
+ if (updates.isEmpty()) {
+ return reply;
+ }
+ updates
+ .stream()
+ .forEach(u -> reply.get(u.getResult().name().toLowerCase()).addAll(toEntryInfos(u)));
+ return reply;
+ }
+
+ private static List<ConfigUpdateEntryInfo> toEntryInfos(ConfigUpdatedEvent.Update update) {
+ return update
+ .getConfigUpdates()
+ .stream()
+ .map(e -> toConfigUpdateEntryInfo(e))
+ .collect(toImmutableList());
+ }
+
+ private static ConfigUpdateEntryInfo toConfigUpdateEntryInfo(ConfigUpdateEntry e) {
+ ConfigUpdateEntryInfo uei = new ConfigUpdateEntryInfo();
+ uei.configKey = e.key.toString();
+ uei.oldValue = e.oldVal;
+ uei.newValue = e.newVal;
+ return uei;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccess.java b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
index 2c0653a..865f077 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
@@ -29,6 +29,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.permissions.DefaultPermissionMappings;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -67,7 +68,8 @@
public AccessCheckInfo apply(ProjectResource rsrc, AccessCheckInput input)
throws OrmException, PermissionBackendException, RestApiException, IOException,
ConfigInvalidException {
- permissionBackend.user(rsrc.getUser()).check(GlobalPermission.ADMINISTRATE_SERVER);
+ permissionBackend.user(rsrc.getUser()).check(GlobalPermission.VIEW_ACCESS);
+
rsrc.getProjectState().checkStatePermitsRead();
if (input == null) {
@@ -102,7 +104,7 @@
if (Strings.isNullOrEmpty(input.ref)) {
throw new BadRequestException("must set 'ref' when specifying 'permission'");
}
- Optional<RefPermission> rp = RefPermission.fromName(input.permission);
+ Optional<RefPermission> rp = DefaultPermissionMappings.refPermission(input.permission);
if (!rp.isPresent()) {
throw new BadRequestException(
String.format("'%s' is not recognized as ref permission", input.permission));
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java b/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java
new file mode 100644
index 0000000..b14a16d
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/CheckAccessReadView.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.gerrit.extensions.api.config.AccessCheckInfo;
+import com.google.gerrit.extensions.api.config.AccessCheckInput;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import javax.inject.Inject;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.kohsuke.args4j.Option;
+
+public class CheckAccessReadView implements RestReadView<ProjectResource> {
+ String refName;
+ String account;
+ String permission;
+
+ @Inject CheckAccess checkAccess;
+
+ @Option(name = "--ref", usage = "ref name to check permission for")
+ void addOption(String refName) {
+ this.refName = refName;
+ }
+
+ @Option(name = "--account", usage = "account to check acccess for")
+ void setAccount(String account) {
+ this.account = account;
+ }
+
+ @Option(name = "--perm", usage = "permission to check; default: read of any ref.")
+ void setPermission(String perm) {
+ this.permission = perm;
+ }
+
+ @Override
+ public AccessCheckInfo apply(ProjectResource rsrc)
+ throws OrmException, PermissionBackendException, RestApiException, IOException,
+ ConfigInvalidException {
+
+ AccessCheckInput input = new AccessCheckInput();
+ input.ref = refName;
+ input.account = account;
+ input.permission = permission;
+
+ return checkAccess.apply(rsrc, input);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index f5bfb94..1529dae 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -119,6 +119,7 @@
try (MetaDataUpdate md = metaDataUpdateUser.create(rsrc.getNameKey())) {
ProjectConfig config = ProjectConfig.read(md);
ObjectId oldCommit = config.getRevision();
+ String oldCommitSha1 = oldCommit == null ? null : oldCommit.getName();
setAccess.validateChanges(config, removals, additions);
setAccess.applyChanges(config, removals, additions);
@@ -141,7 +142,7 @@
config.commitToNewRef(
md, new PatchSet.Id(changeId, Change.INITIAL_PATCH_SET_ID).toRefName());
- if (commit.name().equals(oldCommit.getName())) {
+ if (commit.name().equals(oldCommitSha1)) {
throw new BadRequestException("no change");
}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateBranch.java b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
index 9f3c473..6305d5d 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateBranch.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.restapi.project;
+import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
+
import com.google.gerrit.extensions.api.projects.BranchInfo;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -178,11 +180,17 @@
BranchInfo info = new BranchInfo();
info.ref = ref;
info.revision = revid.getName();
- info.canDelete =
- permissionBackend.currentUser().ref(name).testOrFalse(RefPermission.DELETE)
- && rsrc.getProjectState().statePermitsWrite()
- ? true
- : null;
+
+ if (isConfigRef(name.get())) {
+ // Never allow to delete the meta config branch.
+ info.canDelete = null;
+ } else {
+ info.canDelete =
+ permissionBackend.currentUser().ref(name).testOrFalse(RefPermission.DELETE)
+ && rsrc.getProjectState().statePermitsWrite()
+ ? true
+ : null;
+ }
return info;
} catch (IOException err) {
log.error("Cannot create branch \"" + name + "\"", err);
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
index 89213a0..aed372c 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteBranch.java
@@ -14,9 +14,11 @@
package com.google.gerrit.server.restapi.project;
+import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
import static org.eclipse.jgit.lib.Constants.R_HEADS;
import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -52,6 +54,12 @@
@Override
public Response<?> apply(BranchResource rsrc, Input input)
throws RestApiException, OrmException, IOException, PermissionBackendException {
+ if (isConfigRef(rsrc.getBranchKey().get())) {
+ // Never allow to delete the meta config branch.
+ throw new MethodNotAllowedException(
+ "not allowed to delete branch " + rsrc.getBranchKey().get());
+ }
+
permissionBackend.currentUser().ref(rsrc.getBranchKey()).check(RefPermission.DELETE);
rsrc.getProjectState().checkStatePermitsWrite();
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteRef.java b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
index c51fc56..13b21c9 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteRef.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteRef.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.project;
+import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
import static java.lang.String.format;
import static java.util.stream.Collectors.toList;
import static org.eclipse.jgit.lib.Constants.R_REFS;
@@ -220,16 +221,21 @@
}
command = new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName());
- try {
- permissionBackend
- .currentUser()
- .project(project.getNameKey())
- .ref(refName)
- .check(RefPermission.DELETE);
- } catch (AuthException denied) {
- command.setResult(
- Result.REJECTED_OTHER_REASON,
- "it doesn't exist or you do not have permission to delete it");
+ if (isConfigRef(refName)) {
+ // Never allow to delete the meta config branch.
+ command.setResult(Result.REJECTED_OTHER_REASON, "not allowed to delete branch " + refName);
+ } else {
+ try {
+ permissionBackend
+ .currentUser()
+ .project(project.getNameKey())
+ .ref(refName)
+ .check(RefPermission.DELETE);
+ } catch (AuthException denied) {
+ command.setResult(
+ Result.REJECTED_OTHER_REASON,
+ "it doesn't exist or you do not have permission to delete it");
+ }
}
if (!project.getProjectState().statePermitsWrite()) {
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteTag.java b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
index a3886bc..bd5f444 100644
--- a/java/com/google/gerrit/server/restapi/project/DeleteTag.java
+++ b/java/com/google/gerrit/server/restapi/project/DeleteTag.java
@@ -14,7 +14,10 @@
package com.google.gerrit.server.restapi.project;
+import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
+
import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -44,6 +47,12 @@
public Response<?> apply(TagResource resource, Input input)
throws OrmException, RestApiException, IOException, PermissionBackendException {
String tag = RefUtil.normalizeTagRef(resource.getTagInfo().ref);
+
+ if (isConfigRef(tag)) {
+ // Never allow to delete the meta config branch.
+ throw new MethodNotAllowedException("not allowed to delete " + tag);
+ }
+
permissionBackend
.currentUser()
.project(resource.getNameKey())
diff --git a/java/com/google/gerrit/server/restapi/project/ListBranches.java b/java/com/google/gerrit/server/restapi/project/ListBranches.java
index b6fa6d0..ed9dede 100644
--- a/java/com/google/gerrit/server/restapi/project/ListBranches.java
+++ b/java/com/google/gerrit/server/restapi/project/ListBranches.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.restapi.project;
+import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
+
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
@@ -200,11 +202,16 @@
branches.add(b);
if (!Constants.HEAD.equals(ref.getName())) {
- b.canDelete =
- perm.ref(ref.getName()).testOrFalse(RefPermission.DELETE)
- && rsrc.getProjectState().statePermitsWrite()
- ? true
- : null;
+ if (isConfigRef(ref.getName())) {
+ // Never allow to delete the meta config branch.
+ b.canDelete = null;
+ } else {
+ b.canDelete =
+ perm.ref(ref.getName()).testOrFalse(RefPermission.DELETE)
+ && rsrc.getProjectState().statePermitsWrite()
+ ? true
+ : null;
+ }
}
continue;
}
@@ -247,12 +254,18 @@
BranchInfo info = new BranchInfo();
info.ref = ref.getName();
info.revision = ref.getObjectId() != null ? ref.getObjectId().name() : null;
- info.canDelete =
- !targets.contains(ref.getName())
- && perm.testOrFalse(RefPermission.DELETE)
- && projectState.statePermitsWrite()
- ? true
- : null;
+
+ if (isConfigRef(ref.getName())) {
+ // Never allow to delete the meta config branch.
+ info.canDelete = null;
+ } else {
+ info.canDelete =
+ !targets.contains(ref.getName())
+ && perm.testOrFalse(RefPermission.DELETE)
+ && projectState.statePermitsWrite()
+ ? true
+ : null;
+ }
BranchResource rsrc = new BranchResource(projectState, user, ref);
for (UiAction.Description d : uiActions.from(branchViews, rsrc)) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index 9a8232e..3407d39 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -577,7 +577,8 @@
try {
// Hidden projects(permitsRead = false) should only be accessible by the project owners.
// READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
- // be allowed for other users). Allowing project owners to access here will help them to view
+ // be allowed for other users). Allowing project owners to access here will help them to
+ // view
// and update the config of hidden projects easily.
ProjectPermission permissionToCheck =
state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index 31ec7e1..ec6a99b 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.restapi.project;
+import static com.google.gerrit.reviewdb.client.RefNames.isConfigRef;
+
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.extensions.api.projects.ProjectApi.ListRefsRequest;
import com.google.gerrit.extensions.api.projects.TagInfo;
@@ -41,7 +43,6 @@
import java.util.Comparator;
import java.util.List;
import java.util.Map;
-import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.PersonIdent;
@@ -183,10 +184,16 @@
public static TagInfo createTagInfo(
PermissionBackend.ForRef perm, Ref ref, RevWalk rw, ProjectState projectState, WebLinks links)
- throws MissingObjectException, IOException {
+ throws IOException {
RevObject object = rw.parseAny(ref.getObjectId());
- Boolean canDelete =
- perm.testOrFalse(RefPermission.DELETE) && projectState.statePermitsWrite() ? true : null;
+
+ Boolean canDelete = null;
+ if (!isConfigRef(ref.getName())) {
+ // Never allow to delete the meta config branch.
+ canDelete =
+ perm.testOrFalse(RefPermission.DELETE) && projectState.statePermitsWrite() ? true : null;
+ }
+
List<WebLinkInfo> webLinks = links.getTagLinks(projectState.getName(), ref.getName());
if (object instanceof RevTag) {
// Annotated or signed tag
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index 67380dc..337084c 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -50,6 +50,7 @@
post(PROJECT_KIND, "access").to(SetAccess.class);
put(PROJECT_KIND, "access:review").to(CreateAccessChange.class);
post(PROJECT_KIND, "check.access").to(CheckAccess.class);
+ get(PROJECT_KIND, "check.access").to(CheckAccessReadView.class);
get(PROJECT_KIND, "parent").to(GetParent.class);
put(PROJECT_KIND, "parent").to(SetParent.class);
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index f118ff2..ca7eb06 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -200,7 +200,7 @@
ProjectConfigEntry projectConfigEntry = pluginConfigEntries.get(pluginName, v.getKey());
if (projectConfigEntry != null) {
if (!PARAMETER_NAME_PATTERN.matcher(v.getKey()).matches()) {
- //TODO check why we have this restriction
+ // TODO check why we have this restriction
log.warn(
"Parameter name '{}' must match '{}'",
v.getKey(),
diff --git a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
index 2671aaf..422c749 100644
--- a/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
+++ b/java/com/google/gerrit/server/restapi/project/SetAccessUtil.java
@@ -214,7 +214,7 @@
* @param identifiedUser the user
* @param config the config to modify
* @param projectName the project for which to change access.
- * @param newParentProjectName the new parent to set.
+ * @param newParentProjectName the new parent to set; passing null will make this a nop
* @param checkAdmin if set, verify that user has administrateServer permission
*/
public void setParentName(
diff --git a/java/com/google/gerrit/server/rules/RulesCache.java b/java/com/google/gerrit/server/rules/RulesCache.java
index d7a614d..6ef11fa 100644
--- a/java/com/google/gerrit/server/rules/RulesCache.java
+++ b/java/com/google/gerrit/server/rules/RulesCache.java
@@ -175,7 +175,7 @@
Path jarPath = rulesDir.resolve("rules-" + rulesId.getName() + ".jar");
if (Files.isRegularFile(jarPath)) {
URL[] cp = new URL[] {toURL(jarPath)};
- return save(newEmptyMachine(new URLClassLoader(cp, systemLoader)));
+ return save(newEmptyMachine(URLClassLoader.newInstance(cp, systemLoader)));
}
}
diff --git a/java/com/google/gerrit/server/rules/StoredValue.java b/java/com/google/gerrit/server/rules/StoredValue.java
index c3bc53f..593d474 100644
--- a/java/com/google/gerrit/server/rules/StoredValue.java
+++ b/java/com/google/gerrit/server/rules/StoredValue.java
@@ -58,7 +58,7 @@
public T get(Prolog engine) {
T obj = getOrNull(engine);
if (obj == null) {
- //unless createValue() is overridden, will return null
+ // unless createValue() is overridden, will return null
obj = createValue(engine);
if (obj == null) {
throw new SystemException("No " + key + " available");
diff --git a/java/com/google/gerrit/server/schema/BUILD b/java/com/google/gerrit/server/schema/BUILD
index 2292234..32a14db 100644
--- a/java/com/google/gerrit/server/schema/BUILD
+++ b/java/com/google/gerrit/server/schema/BUILD
@@ -16,6 +16,7 @@
"//lib:guava",
"//lib:gwtorm",
"//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/commons:dbcp",
"//lib/guice",
"//lib/jgit/org.eclipse.jgit.archive:jgit-archive",
diff --git a/java/com/google/gerrit/server/schema/MySql.java b/java/com/google/gerrit/server/schema/MySql.java
index f4f19f0..e5f59d7 100644
--- a/java/com/google/gerrit/server/schema/MySql.java
+++ b/java/com/google/gerrit/server/schema/MySql.java
@@ -41,7 +41,8 @@
b.append(port(dbs.optional("port")));
b.append("/");
b.append(dbs.required("database"));
- // See https://stackoverflow.com/questions/42084633/table-name-pattern-can-not-be-null-or-empty-in-java
+ // See
+ // https://stackoverflow.com/questions/42084633/table-name-pattern-can-not-be-null-or-empty-in-java
b.append("?nullNamePatternMatchesAll=true");
return b.toString();
}
diff --git a/java/com/google/gerrit/server/schema/Schema_105.java b/java/com/google/gerrit/server/schema/Schema_105.java
index 78ecdbd..dd5e71a7 100644
--- a/java/com/google/gerrit/server/schema/Schema_105.java
+++ b/java/com/google/gerrit/server/schema/Schema_105.java
@@ -66,7 +66,8 @@
private Set<String> listChangesIndexes(JdbcSchema schema) throws SQLException {
// List of all changes indexes ever created or dropped, found with the
// following command:
- // find g* -name \*.sql | xargs git log -i -p -S' index changes_' | grep -io ' index changes_\w*' | cut -d' ' -f3 | tr A-Z a-z | sort -u
+ // find g* -name \*.sql | xargs git log -i -p -S' index changes_' | grep -io ' index
+ // changes_\w*' | cut -d' ' -f3 | tr A-Z a-z | sort -u
// Used rather than listIndexes as we're not sure whether it might include
// primary key indexes.
Set<String> allChanges =
diff --git a/java/com/google/gerrit/server/schema/Schema_124.java b/java/com/google/gerrit/server/schema/Schema_124.java
index 497d5f2..8746427 100644
--- a/java/com/google/gerrit/server/schema/Schema_124.java
+++ b/java/com/google/gerrit/server/schema/Schema_124.java
@@ -84,11 +84,8 @@
Account.Id accountId = new Account.Id(rs.getInt(1));
int seq = rs.getInt(2);
String sshPublicKey = rs.getString(3);
- AccountSshKey key = new AccountSshKey(new AccountSshKey.Id(accountId, seq), sshPublicKey);
boolean valid = toBoolean(rs.getString(4));
- if (!valid) {
- key.setInvalid();
- }
+ AccountSshKey key = AccountSshKey.create(accountId, seq, sshPublicKey, valid);
imports.put(accountId, key);
}
}
@@ -122,15 +119,13 @@
}
private Collection<AccountSshKey> fixInvalidSequenceNumbers(Collection<AccountSshKey> keys) {
- Ordering<AccountSshKey> o = Ordering.from(comparing(k -> k.getKey().get()));
+ Ordering<AccountSshKey> o = Ordering.from(comparing(k -> k.seq()));
List<AccountSshKey> fixedKeys = new ArrayList<>(keys);
AccountSshKey minKey = o.min(keys);
- while (minKey.getKey().get() <= 0) {
+ while (minKey.seq() <= 0) {
AccountSshKey fixedKey =
- new AccountSshKey(
- new AccountSshKey.Id(
- minKey.getKey().getParentKey(), Math.max(o.max(keys).getKey().get() + 1, 1)),
- minKey.getSshPublicKey());
+ AccountSshKey.create(
+ minKey.accountId(), Math.max(o.max(keys).seq() + 1, 1), minKey.sshPublicKey());
Collections.replaceAll(fixedKeys, minKey, fixedKey);
minKey = o.min(fixedKeys);
}
diff --git a/java/com/google/gerrit/server/ssh/NoSshKeyCache.java b/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
index 308d0cc..387242c 100644
--- a/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
+++ b/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.ssh;
import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.AccountSshKey;
import com.google.inject.AbstractModule;
import com.google.inject.Module;
@@ -37,7 +38,8 @@
public void evict(String username) {}
@Override
- public AccountSshKey create(AccountSshKey.Id id, String encoded) throws InvalidSshKeyException {
+ public AccountSshKey create(Account.Id accountId, int seq, String encoded)
+ throws InvalidSshKeyException {
throw new InvalidSshKeyException();
}
}
diff --git a/java/com/google/gerrit/server/ssh/SshKeyCreator.java b/java/com/google/gerrit/server/ssh/SshKeyCreator.java
index d078c43..55ba5ed 100644
--- a/java/com/google/gerrit/server/ssh/SshKeyCreator.java
+++ b/java/com/google/gerrit/server/ssh/SshKeyCreator.java
@@ -15,8 +15,9 @@
package com.google.gerrit.server.ssh;
import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.AccountSshKey;
public interface SshKeyCreator {
- AccountSshKey create(AccountSshKey.Id id, String encoded) throws InvalidSshKeyException;
+ AccountSshKey create(Account.Id accountId, int seq, String encoded) throws InvalidSshKeyException;
}
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index c8e4e1d..aceb824 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -24,7 +24,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.mail.SendEmailExecutor;
+import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.mail.send.MergedSender;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
diff --git a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
index e60869a..fa20ad9 100644
--- a/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
+++ b/java/com/google/gerrit/server/submit/LocalMergeSuperSetComputation.java
@@ -215,8 +215,7 @@
List<ChangeData> result = new ArrayList<>();
Iterable<ChangeData> destChanges =
- MergeSuperSet.query(queryProvider.get())
- .byCommitsOnBranchNotMerged(or.repo, db, branch, hashes);
+ queryProvider.get().byCommitsOnBranchNotMerged(or.repo, db, branch, hashes);
for (ChangeData chd : destChanges) {
result.add(chd);
}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 6b663cc..7dd1ac9 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -235,6 +235,7 @@
private final Provider<MergeOpRepoManager> ormProvider;
private final NotifyUtil notifyUtil;
private final RetryHelper retryHelper;
+ private final ChangeData.Factory changeDataFactory;
private Timestamp ts;
private RequestId submissionId;
@@ -262,7 +263,8 @@
Provider<MergeOpRepoManager> ormProvider,
NotifyUtil notifyUtil,
TopicMetrics topicMetrics,
- RetryHelper retryHelper) {
+ RetryHelper retryHelper,
+ ChangeData.Factory changeDataFactory) {
this.cmUtil = cmUtil;
this.batchUpdateFactory = batchUpdateFactory;
this.internalUserFactory = internalUserFactory;
@@ -275,6 +277,7 @@
this.notifyUtil = notifyUtil;
this.retryHelper = retryHelper;
this.topicMetrics = topicMetrics;
+ this.changeDataFactory = changeDataFactory;
}
@Override
@@ -443,14 +446,21 @@
logDebug("Beginning integration of {}", change);
try {
- ChangeSet cs = mergeSuperSet.setMergeOpRepoManager(orm).completeChangeSet(db, change, caller);
+ ChangeSet indexBackedChangeSet =
+ mergeSuperSet.setMergeOpRepoManager(orm).completeChangeSet(db, change, caller);
checkState(
- cs.ids().contains(change.getId()), "change %s missing from %s", change.getId(), cs);
- if (cs.furtherHiddenChanges()) {
+ indexBackedChangeSet.ids().contains(change.getId()),
+ "change %s missing from %s",
+ change.getId(),
+ indexBackedChangeSet);
+ if (indexBackedChangeSet.furtherHiddenChanges()) {
throw new AuthException(
"A change to be submitted with " + change.getId() + " is not visible");
}
- logDebug("Calculated to merge {}", cs);
+ logDebug("Calculated to merge {}", indexBackedChangeSet);
+
+ // Reload ChangeSet so that we don't rely on (potentially) stale index data for merging
+ ChangeSet cs = reloadChanges(indexBackedChangeSet);
// Count cross-project submissions outside of the retry loop. The chance of a single project
// failing increases with the number of projects, so the failure count would be inflated if
@@ -471,7 +481,6 @@
openRepoManager();
}
this.commitStatus = new CommitStatus(cs, isRetry);
- MergeSuperSet.reloadChanges(cs);
if (checkSubmitRules) {
logDebug("Checking submit rules and state");
checkSubmitRulesAndState(cs, isRetry);
@@ -514,6 +523,18 @@
orm.setContext(db, ts, caller, submissionId);
}
+ private ChangeSet reloadChanges(ChangeSet changeSet) {
+ List<ChangeData> visible = new ArrayList<>(changeSet.changes().size());
+ List<ChangeData> nonVisible = new ArrayList<>(changeSet.nonVisibleChanges().size());
+ changeSet
+ .changes()
+ .forEach(c -> visible.add(changeDataFactory.create(db, c.project(), c.getId())));
+ changeSet
+ .nonVisibleChanges()
+ .forEach(c -> nonVisible.add(changeDataFactory.create(db, c.project(), c.getId())));
+ return new ChangeSet(visible, nonVisible);
+ }
+
private class RetryTracker implements RetryListener {
long lastAttemptNumber;
diff --git a/java/com/google/gerrit/server/submit/MergeSuperSet.java b/java/com/google/gerrit/server/submit/MergeSuperSet.java
index 6ffe4fb..3e9f068 100644
--- a/java/com/google/gerrit/server/submit/MergeSuperSet.java
+++ b/java/com/google/gerrit/server/submit/MergeSuperSet.java
@@ -18,12 +18,12 @@
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -53,25 +53,6 @@
*/
public class MergeSuperSet {
- public static void reloadChanges(ChangeSet changeSet) throws OrmException {
- // Clear exactly the fields requested by query(InternalChangeQuery) below.
- for (ChangeData cd : changeSet.changes()) {
- cd.reloadChange();
- cd.setPatchSets(null);
- cd.setMergeable(null);
- }
- }
-
- public static InternalChangeQuery query(InternalChangeQuery q) {
- // Request fields required for completing the ChangeSet and converting to
- // ChangeInfo without having to touch the database or opening the repository
- // more than necessary. This provides reasonable performance when loading
- // the change screen; callers that care about reading the latest value of
- // these fields should clear them explicitly using reloadChanges().
- return q.setRequestedFields(ChangeField.CHANGE, ChangeField.PATCH_SET, ChangeField.MERGEABLE);
- }
-
- private final ChangeData.Factory changeDataFactory;
private final Provider<InternalChangeQuery> queryProvider;
private final Provider<MergeOpRepoManager> repoManagerProvider;
private final DynamicItem<MergeSuperSetComputation> mergeSuperSetComputation;
@@ -85,14 +66,12 @@
@Inject
MergeSuperSet(
@GerritServerConfig Config cfg,
- ChangeData.Factory changeDataFactory,
Provider<InternalChangeQuery> queryProvider,
Provider<MergeOpRepoManager> repoManagerProvider,
DynamicItem<MergeSuperSetComputation> mergeSuperSetComputation,
PermissionBackend permissionBackend,
ProjectCache projectCache) {
this.cfg = cfg;
- this.changeDataFactory = changeDataFactory;
this.queryProvider = queryProvider;
this.repoManagerProvider = repoManagerProvider;
this.mergeSuperSetComputation = mergeSuperSetComputation;
@@ -118,8 +97,9 @@
orm = repoManagerProvider.get();
closeOrm = true;
}
-
- ChangeData cd = changeDataFactory.create(db, change.getProject(), change.getId());
+ List<ChangeData> cds = queryProvider.get().byLegacyChangeId(change.getId());
+ checkState(cds.size() == 1, "Expected exactly one ChangeData, got " + cds.size());
+ ChangeData cd = Iterables.getFirst(cds, null);
ProjectState projectState = projectCache.checkedGet(cd.project());
ChangeSet changeSet =
new ChangeSet(
@@ -217,7 +197,7 @@
}
private List<ChangeData> byTopicOpen(String topic) throws OrmException {
- return query(queryProvider.get()).byTopicOpen(topic);
+ return queryProvider.get().byTopicOpen(topic);
}
private boolean canRead(ReviewDb db, CurrentUser user, ChangeData cd)
diff --git a/java/com/google/gerrit/server/update/RepoOnlyOp.java b/java/com/google/gerrit/server/update/RepoOnlyOp.java
index 10a6a31..7e9c47e 100644
--- a/java/com/google/gerrit/server/update/RepoOnlyOp.java
+++ b/java/com/google/gerrit/server/update/RepoOnlyOp.java
@@ -34,6 +34,6 @@
*
* @param ctx context
*/
- //TODO(dborowitz): Support async operations?
+ // TODO(dborowitz): Support async operations?
default void postUpdate(Context ctx) throws Exception {}
}
diff --git a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java b/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
index 07ae04d..9cdb006 100644
--- a/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
+++ b/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
@@ -43,6 +43,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.ChangeUpdateExecutor;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
diff --git a/java/com/google/gerrit/sshd/BUILD b/java/com/google/gerrit/sshd/BUILD
index 3ed1f2f..c36d68b 100644
--- a/java/com/google/gerrit/sshd/BUILD
+++ b/java/com/google/gerrit/sshd/BUILD
@@ -25,6 +25,7 @@
"//lib:jsch",
"//lib:servlet-api-3_1",
"//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/bouncycastle:bcprov-neverlink",
"//lib/commons:codec",
"//lib/dropwizard:dropwizard-core",
diff --git a/java/com/google/gerrit/sshd/SshKeyCacheEntry.java b/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
index 206b279..8e962e3 100644
--- a/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
+++ b/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
@@ -15,20 +15,19 @@
package com.google.gerrit.sshd;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.account.AccountSshKey;
import java.security.PublicKey;
class SshKeyCacheEntry {
- private final AccountSshKey.Id id;
+ private final Account.Id accountId;
private final PublicKey publicKey;
- SshKeyCacheEntry(AccountSshKey.Id i, PublicKey k) {
- id = i;
- publicKey = k;
+ SshKeyCacheEntry(Account.Id accountId, PublicKey publicKey) {
+ this.accountId = accountId;
+ this.publicKey = publicKey;
}
Account.Id getAccount() {
- return id.getParentKey();
+ return accountId;
}
boolean match(PublicKey inkey) {
diff --git a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 2f039f1..3ab7a58 100644
--- a/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -35,6 +35,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.ExecutionException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
@@ -101,14 +102,14 @@
@Override
public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
- ExternalId user = externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, username));
- if (user == null) {
+ Optional<ExternalId> user = externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, username));
+ if (!user.isPresent()) {
return NO_SUCH_USER;
}
List<SshKeyCacheEntry> kl = new ArrayList<>(4);
- for (AccountSshKey k : authorizedKeys.getKeys(user.accountId())) {
- if (k.isValid()) {
+ for (AccountSshKey k : authorizedKeys.getKeys(user.get().accountId())) {
+ if (k.valid()) {
add(kl, k);
}
}
@@ -121,7 +122,7 @@
private void add(List<SshKeyCacheEntry> kl, AccountSshKey k) {
try {
- kl.add(new SshKeyCacheEntry(k.getKey(), SshUtil.parse(k)));
+ kl.add(new SshKeyCacheEntry(k.accountId(), SshUtil.parse(k)));
} catch (OutOfMemoryError e) {
// This is the only case where we assume the problem has nothing
// to do with the key object, and instead we must abort this load.
@@ -134,11 +135,11 @@
private void markInvalid(AccountSshKey k) {
try {
- log.info("Flagging SSH key " + k.getKey() + " invalid");
- authorizedKeys.markKeyInvalid(k.getAccount(), k.getKey().get());
- k.setInvalid();
+ log.info("Flagging SSH key " + k.seq() + " of account " + k.accountId() + " invalid");
+ authorizedKeys.markKeyInvalid(k.accountId(), k.seq());
} catch (IOException | ConfigInvalidException e) {
- log.error("Failed to mark SSH key" + k.getKey() + " invalid", e);
+ log.error(
+ "Failed to mark SSH key " + k.seq() + " of account " + k.accountId() + " invalid", e);
}
}
}
diff --git a/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java b/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
index b838e07..bb47e3f 100644
--- a/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
+++ b/java/com/google/gerrit/sshd/SshKeyCreatorImpl.java
@@ -15,6 +15,7 @@
package com.google.gerrit.sshd;
import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.AccountSshKey;
import com.google.gerrit.server.ssh.SshKeyCreator;
import java.security.NoSuchAlgorithmException;
@@ -27,9 +28,10 @@
private static final Logger log = LoggerFactory.getLogger(SshKeyCreatorImpl.class);
@Override
- public AccountSshKey create(AccountSshKey.Id id, String encoded) throws InvalidSshKeyException {
+ public AccountSshKey create(Account.Id accountId, int seq, String encoded)
+ throws InvalidSshKeyException {
try {
- AccountSshKey key = new AccountSshKey(id, SshUtil.toOpenSshPublicKey(encoded));
+ AccountSshKey key = AccountSshKey.create(accountId, seq, SshUtil.toOpenSshPublicKey(encoded));
SshUtil.parse(key);
return key;
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
diff --git a/java/com/google/gerrit/sshd/SshLog.java b/java/com/google/gerrit/sshd/SshLog.java
index 6465a30..b6c2d19 100644
--- a/java/com/google/gerrit/sshd/SshLog.java
+++ b/java/com/google/gerrit/sshd/SshLog.java
@@ -23,6 +23,9 @@
import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.audit.AuditService;
import com.google.gerrit.server.audit.SshAuditEvent;
+import com.google.gerrit.server.config.ConfigKey;
+import com.google.gerrit.server.config.ConfigUpdatedEvent;
+import com.google.gerrit.server.config.GerritConfigListener;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.util.IdGenerator;
import com.google.gerrit.server.util.SystemLog;
@@ -30,6 +33,8 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.util.Collections;
+import java.util.List;
import org.apache.log4j.AsyncAppender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
@@ -37,7 +42,7 @@
import org.eclipse.jgit.lib.Config;
@Singleton
-class SshLog implements LifecycleListener {
+class SshLog implements LifecycleListener, GerritConfigListener {
private static final Logger log = Logger.getLogger(SshLog.class);
private static final String LOG_NAME = "sshd_log";
private static final String P_SESSION = "session";
@@ -50,8 +55,11 @@
private final Provider<SshSession> session;
private final Provider<Context> context;
- private final AsyncAppender async;
+ private volatile AsyncAppender async;
private final AuditService auditService;
+ private final SystemLog systemLog;
+
+ private final Object lock = new Object();
@Inject
SshLog(
@@ -63,12 +71,34 @@
this.session = session;
this.context = context;
this.auditService = auditService;
+ this.systemLog = systemLog;
- if (!config.getBoolean("sshd", "requestLog", true)) {
- async = null;
- return;
+ if (config.getBoolean("sshd", "requestLog", true)) {
+ enableLogging();
}
- async = systemLog.createAsyncAppender(LOG_NAME, new SshLogLayout());
+ }
+
+ /** @return true if a change in state has occurred */
+ public boolean enableLogging() {
+ synchronized (lock) {
+ if (async == null) {
+ async = systemLog.createAsyncAppender(LOG_NAME, new SshLogLayout());
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /** @return true if a change in state has occurred */
+ public boolean disableLogging() {
+ synchronized (lock) {
+ if (async != null) {
+ async.close();
+ async = null;
+ return true;
+ }
+ return false;
+ }
}
@Override
@@ -76,9 +106,7 @@
@Override
public void stop() {
- if (async != null) {
- async.close();
- }
+ disableLogging();
}
void onLogin() {
@@ -288,4 +316,23 @@
}
return commandName.toString();
}
+
+ @Override
+ public List<ConfigUpdatedEvent.Update> configUpdated(ConfigUpdatedEvent event) {
+ ConfigKey sshdRequestLog = ConfigKey.create("sshd", "requestLog");
+ if (!event.isValueUpdated(sshdRequestLog)) {
+ return Collections.emptyList();
+ }
+
+ boolean enabled = event.getNewConfig().getBoolean("sshd", "requestLog", true);
+ boolean stateUpdated;
+ if (enabled) {
+ stateUpdated = enableLogging();
+ } else {
+ stateUpdated = disableLogging();
+ }
+ return stateUpdated
+ ? Collections.singletonList(event.accept(sshdRequestLog))
+ : Collections.emptyList();
+ }
}
diff --git a/java/com/google/gerrit/sshd/SshModule.java b/java/com/google/gerrit/sshd/SshModule.java
index 03ed74f..27a431c 100644
--- a/java/com/google/gerrit/sshd/SshModule.java
+++ b/java/com/google/gerrit/sshd/SshModule.java
@@ -21,10 +21,12 @@
import com.google.common.base.Splitter;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.DynamicOptions;
import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.RemotePeer;
+import com.google.gerrit.server.config.GerritConfigListener;
import com.google.gerrit.server.config.GerritRequestModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.QueueProvider;
@@ -71,6 +73,8 @@
configureAliases();
bind(SshLog.class);
+ DynamicSet.bind(binder(), GerritConfigListener.class).to(SshLog.class);
+
bind(SshInfo.class).to(SshDaemon.class).in(SINGLETON);
factory(DispatchCommand.Factory.class);
factory(QueryShell.Factory.class);
diff --git a/java/com/google/gerrit/sshd/SshUtil.java b/java/com/google/gerrit/sshd/SshUtil.java
index 6fb83f0..b37ca8b 100644
--- a/java/com/google/gerrit/sshd/SshUtil.java
+++ b/java/com/google/gerrit/sshd/SshUtil.java
@@ -51,7 +51,7 @@
public static PublicKey parse(AccountSshKey key)
throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException {
try {
- final String s = key.getEncodedKey();
+ final String s = key.encodedKey();
if (s == null) {
throw new InvalidKeySpecException("No key string");
}
diff --git a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index 85a108a..0e36d53 100644
--- a/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -53,6 +53,7 @@
command(gerrit, ListGroupsCommand.class);
command(gerrit, LsUserRefs.class);
command(gerrit, Query.class);
+ command(gerrit, ReloadConfig.class);
command(gerrit, ShowCaches.class);
command(gerrit, ShowConnections.class);
command(gerrit, ShowQueue.class);
diff --git a/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java b/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
index 6b3e6d7..0804d08 100644
--- a/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexActivateCommand.java
@@ -16,8 +16,8 @@
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.lucene.LuceneVersionManager;
import com.google.gerrit.server.index.ReindexerAlreadyRunningException;
+import com.google.gerrit.server.index.VersionManager;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
@@ -30,15 +30,19 @@
@Argument(index = 0, required = true, metaVar = "INDEX", usage = "index name to activate")
private String name;
- @Inject private LuceneVersionManager luceneVersionManager;
+ @Inject private VersionManager versionManager;
@Override
protected void run() throws UnloggedFailure {
try {
- if (luceneVersionManager.activateLatestIndex(name)) {
- stdout.println("Activated latest index version");
+ if (versionManager.isKnownIndex(name)) {
+ if (versionManager.activateLatestIndex(name)) {
+ stdout.println("Activated latest index version");
+ } else {
+ stdout.println("Not activating index, already using latest version");
+ }
} else {
- stdout.println("Not activating index, already using latest version");
+ stderr.println(String.format("Cannot activate index %s: unknown", name));
}
} catch (ReindexerAlreadyRunningException e) {
throw die("Failed to activate latest index: " + e.getMessage());
diff --git a/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java b/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
index d00468a..599c9dc 100644
--- a/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
+++ b/java/com/google/gerrit/sshd/commands/IndexCommandsModule.java
@@ -14,20 +14,31 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.server.index.VersionManager;
import com.google.gerrit.sshd.CommandModule;
import com.google.gerrit.sshd.CommandName;
import com.google.gerrit.sshd.Commands;
import com.google.gerrit.sshd.DispatchCommandProvider;
+import com.google.inject.Injector;
+import com.google.inject.Key;
public class IndexCommandsModule extends CommandModule {
+ private final Injector injector;
+
+ public IndexCommandsModule(Injector injector) {
+ this.injector = injector;
+ }
+
@Override
protected void configure() {
CommandName gerrit = Commands.named("gerrit");
CommandName index = Commands.named(gerrit, "index");
command(index).toProvider(new DispatchCommandProvider(index));
- command(index, IndexActivateCommand.class);
- command(index, IndexStartCommand.class);
+ if (injector.getExistingBinding(Key.get(VersionManager.class)) != null) {
+ command(index, IndexActivateCommand.class);
+ command(index, IndexStartCommand.class);
+ }
command(index, IndexChangesCommand.class);
command(index, IndexProjectCommand.class);
}
diff --git a/java/com/google/gerrit/sshd/commands/IndexStartCommand.java b/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
index fb9b482..f3d349c 100644
--- a/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
+++ b/java/com/google/gerrit/sshd/commands/IndexStartCommand.java
@@ -16,8 +16,8 @@
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.lucene.LuceneVersionManager;
import com.google.gerrit.server.index.ReindexerAlreadyRunningException;
+import com.google.gerrit.server.index.VersionManager;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
@@ -34,15 +34,19 @@
@Argument(index = 0, required = true, metaVar = "INDEX", usage = "index name to start")
private String name;
- @Inject private LuceneVersionManager luceneVersionManager;
+ @Inject private VersionManager versionManager;
@Override
protected void run() throws UnloggedFailure {
try {
- if (luceneVersionManager.startReindexer(name, force)) {
- stdout.println("Reindexer started");
+ if (versionManager.isKnownIndex(name)) {
+ if (versionManager.startReindexer(name, force)) {
+ stdout.println("Reindexer started");
+ } else {
+ stdout.println("Nothing to reindex, index is already the latest version");
+ }
} else {
- stdout.println("Nothing to reindex, index is already the latest version");
+ stderr.println(String.format("Cannot reindex %s: unknown", name));
}
} catch (ReindexerAlreadyRunningException e) {
throw die("Failed to start reindexer: " + e.getMessage());
diff --git a/java/com/google/gerrit/sshd/commands/ReloadConfig.java b/java/com/google/gerrit/sshd/commands/ReloadConfig.java
new file mode 100644
index 0000000..20145d2
--- /dev/null
+++ b/java/com/google/gerrit/sshd/commands/ReloadConfig.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.config.ConfigUpdatedEvent;
+import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
+import com.google.gerrit.server.config.GerritServerConfigReloader;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/** Issues a reload of gerrit.config. */
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(
+ name = "reload-config",
+ description = "Reloads the Gerrit configuration",
+ runsAt = MASTER_OR_SLAVE
+)
+public class ReloadConfig extends SshCommand {
+
+ @Inject private GerritServerConfigReloader gerritServerConfigReloader;
+
+ @Override
+ protected void run() throws Failure {
+ List<ConfigUpdatedEvent.Update> updates = gerritServerConfigReloader.reloadConfig();
+ if (updates.isEmpty()) {
+ stdout.println("No config entries updated!");
+ return;
+ }
+
+ // Print out UpdateResult.{ACCEPTED|REJECTED} entries grouped by their type
+ for (UpdateResult updateResult : UpdateResult.values()) {
+ List<ConfigUpdatedEvent.Update> filteredUpdates = filterUpdates(updates, updateResult);
+ if (filteredUpdates.isEmpty()) {
+ continue;
+ }
+ stdout.println(updateResult.toString() + " configuration changes:");
+ filteredUpdates
+ .stream()
+ .flatMap(update -> update.getConfigUpdates().stream())
+ .forEach(cfgEntry -> stdout.println(cfgEntry.toString()));
+ }
+ }
+
+ public static List<ConfigUpdatedEvent.Update> filterUpdates(
+ List<ConfigUpdatedEvent.Update> updates, UpdateResult result) {
+ return updates
+ .stream()
+ .filter(update -> update.getResult() == result)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 04f7d3c..bc1e084 100644
--- a/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -263,8 +263,7 @@
private void deleteSshKey(SshKeyInfo i)
throws AuthException, OrmException, RepositoryNotFoundException, IOException,
ConfigInvalidException, PermissionBackendException {
- AccountSshKey sshKey =
- new AccountSshKey(new AccountSshKey.Id(user.getAccountId(), i.seq), i.sshPublicKey);
+ AccountSshKey sshKey = AccountSshKey.create(user.getAccountId(), i.seq, i.sshPublicKey);
deleteSshKey.apply(new AccountResource.SshKey(user.asIdentifiedUser(), sshKey), null);
}
diff --git a/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java b/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
index 4957a60..bea4da13 100644
--- a/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetLoggingLevelCommand.java
@@ -66,8 +66,7 @@
reset();
} else {
for (Enumeration<Logger> logger = LogManager.getCurrentLoggers();
- logger.hasMoreElements();
- ) {
+ logger.hasMoreElements(); ) {
Logger log = logger.nextElement();
if (name == null || log.getName().contains(name)) {
log.setLevel(Level.toLevel(level.name()));
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 9846825..f2fe4c2 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -26,12 +26,14 @@
"//java/com/google/gerrit/server:module",
"//java/com/google/gerrit/server/api",
"//java/com/google/gerrit/server/cache/h2",
+ "//java/com/google/gerrit/server/cache/mem",
"//java/com/google/gerrit/server/restapi",
"//java/com/google/gerrit/server/schema",
"//lib:gwtorm",
"//lib:h2",
"//lib:truth",
"//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/guice",
"//lib/guice:guice-servlet",
"//lib/jgit/org.eclipse.jgit:jgit",
diff --git a/java/com/google/gerrit/testing/FakeAccountCache.java b/java/com/google/gerrit/testing/FakeAccountCache.java
index e549e08..224a5bf 100644
--- a/java/com/google/gerrit/testing/FakeAccountCache.java
+++ b/java/com/google/gerrit/testing/FakeAccountCache.java
@@ -14,6 +14,8 @@
package com.google.gerrit.testing;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account;
@@ -24,6 +26,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
/** Fake implementation of {@link AccountCache} for testing. */
public class FakeAccountCache implements AccountCache {
@@ -48,6 +51,11 @@
}
@Override
+ public synchronized Map<Account.Id, AccountState> get(Set<Account.Id> accountIds) {
+ return ImmutableMap.copyOf(Maps.filterKeys(byId, accountIds::contains));
+ }
+
+ @Override
public synchronized Optional<AccountState> getByUsername(String username) {
throw new UnsupportedOperationException();
}
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index 471f4fa..b63830e 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -29,11 +29,13 @@
import com.google.gerrit.metrics.DisabledMetricMaker;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.FanOutExecutor;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.api.GerritApiModule;
import com.google.gerrit.server.api.PluginApiModule;
-import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
+import com.google.gerrit.server.cache.h2.H2CacheModule;
+import com.google.gerrit.server.cache.mem.DefaultMemoryCacheModule;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllProjectsNameProvider;
import com.google.gerrit.server.config.AllUsersName;
@@ -42,12 +44,14 @@
import com.google.gerrit.server.config.AnonymousCowardNameProvider;
import com.google.gerrit.server.config.CanonicalWebUrlModule;
import com.google.gerrit.server.config.CanonicalWebUrlProvider;
+import com.google.gerrit.server.config.ChangeUpdateExecutor;
import com.google.gerrit.server.config.GerritGlobalModule;
import com.google.gerrit.server.config.GerritInstanceNameModule;
import com.google.gerrit.server.config.GerritOptions;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.GerritServerId;
import com.google.gerrit.server.config.GerritServerIdProvider;
+import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.config.SitePath;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.config.TrackingFootersProvider;
@@ -62,7 +66,6 @@
import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
import com.google.gerrit.server.index.group.AllGroupsIndexer;
import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
-import com.google.gerrit.server.mail.SendEmailExecutor;
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
import com.google.gerrit.server.notedb.ChangeBundleReader;
import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
@@ -82,7 +85,6 @@
import com.google.gerrit.server.securestore.SecureStore;
import com.google.gerrit.server.ssh.NoSshKeyCache;
import com.google.gerrit.server.submit.LocalMergeSuperSetComputation;
-import com.google.gerrit.server.update.ChangeUpdateExecutor;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.AbstractModule;
@@ -227,7 +229,8 @@
return MoreExecutors.newDirectExecutorService();
}
});
- install(new DefaultCacheFactory.Module());
+ install(new DefaultMemoryCacheModule());
+ install(new H2CacheModule());
install(new FakeEmailSender.Module());
install(new SignedTokenEmailTokenVerifier.Module());
install(new GpgModule(cfg));
@@ -271,6 +274,13 @@
@Provides
@Singleton
+ @FanOutExecutor
+ public ExecutorService createChangeJsonExecutor() {
+ return MoreExecutors.newDirectExecutorService();
+ }
+
+ @Provides
+ @Singleton
@GerritServerId
public String createServerId() {
String serverId =
diff --git a/javatests/com/google/gerrit/server/query/IndexConfig.java b/java/com/google/gerrit/testing/IndexConfig.java
similarity index 63%
rename from javatests/com/google/gerrit/server/query/IndexConfig.java
rename to java/com/google/gerrit/testing/IndexConfig.java
index 87452b5..9cace88 100644
--- a/javatests/com/google/gerrit/server/query/IndexConfig.java
+++ b/java/com/google/gerrit/testing/IndexConfig.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 The Android Open Source Project
+// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -12,11 +12,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.query;
+package com.google.gerrit.testing;
import org.eclipse.jgit.lib.Config;
public class IndexConfig {
+ public static Config create() {
+ return createFromExistingConfig(new Config());
+ }
+
+ public static Config createFromExistingConfig(Config cfg) {
+ cfg.setInt("index", null, "maxPages", 10);
+ cfg.setString("trackingid", "query-bug", "footer", "Bug:");
+ cfg.setString("trackingid", "query-bug", "match", "QUERY\\d{2,8}");
+ cfg.setString("trackingid", "query-bug", "system", "querytests");
+ cfg.setString("trackingid", "query-feature", "footer", "Feature");
+ cfg.setString("trackingid", "query-feature", "match", "QUERY\\d{2,8}");
+ cfg.setString("trackingid", "query-feature", "system", "querytests");
+ return cfg;
+ }
public static Config createForLucene() {
return create();
@@ -31,10 +45,4 @@
return cfg;
}
-
- public static Config create() {
- Config cfg = new Config();
- cfg.setInt("index", null, "maxPages", 10);
- return cfg;
- }
}
diff --git a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
index fce28de..bed2504 100644
--- a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
+++ b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
@@ -20,12 +20,16 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupIncludeCache;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AllUsersNameProvider;
import com.google.gerrit.server.index.account.AccountIndexer;
+import com.google.gerrit.server.index.group.GroupIndexer;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.testing.GerritBaseTests;
import com.google.gerrit.testing.InMemoryRepositoryManager;
@@ -224,7 +228,7 @@
Ref nonMetaConfig = createRef("refs/heads/master");
try (ProjectResetter resetProject =
- builder(null, null, null, projectCache)
+ builder(null, null, null, null, null, null, projectCache)
.build(new ProjectResetter.Config().reset(project).reset(project2))) {
updateRef(nonMetaConfig);
updateRef(repo2, metaConfig);
@@ -244,7 +248,7 @@
EasyMock.replay(projectCache);
try (ProjectResetter resetProject =
- builder(null, null, null, projectCache)
+ builder(null, null, null, null, null, null, projectCache)
.build(new ProjectResetter.Config().reset(project).reset(project2))) {
createRef("refs/heads/master");
createRef(repo2, RefNames.REFS_CONFIG);
@@ -274,7 +278,7 @@
Ref nonUserBranch = createRef(RefNames.refsUsers(new Account.Id(2)));
try (ProjectResetter resetProject =
- builder(null, accountCache, accountIndexer, null)
+ builder(null, accountCache, accountIndexer, null, null, null, null)
.build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
updateRef(nonUserBranch);
updateRef(allUsersRepo, userBranch);
@@ -300,7 +304,7 @@
EasyMock.replay(accountIndexer);
try (ProjectResetter resetProject =
- builder(null, accountCache, accountIndexer, null)
+ builder(null, accountCache, accountIndexer, null, null, null, null)
.build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
// Non-user branch because it's not in All-Users.
createRef(RefNames.refsUsers(new Account.Id(2)));
@@ -339,7 +343,7 @@
Ref nonUserBranch = createRef(RefNames.refsUsers(new Account.Id(3)));
try (ProjectResetter resetProject =
- builder(null, accountCache, accountIndexer, null)
+ builder(null, accountCache, accountIndexer, null, null, null, null)
.build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
updateRef(nonUserBranch);
updateRef(allUsersRepo, externalIds);
@@ -376,7 +380,7 @@
Ref nonUserBranch = createRef(RefNames.refsUsers(new Account.Id(3)));
try (ProjectResetter resetProject =
- builder(null, accountCache, accountIndexer, null)
+ builder(null, accountCache, accountIndexer, null, null, null, null)
.build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
updateRef(nonUserBranch);
createRef(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
@@ -398,7 +402,7 @@
EasyMock.replay(accountCreator);
try (ProjectResetter resetProject =
- builder(accountCreator, null, null, null)
+ builder(accountCreator, null, null, null, null, null, null)
.build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
createRef(allUsersRepo, RefNames.refsUsers(accountId));
}
@@ -406,6 +410,39 @@
EasyMock.verify(accountCreator);
}
+ @Test
+ public void groupEviction() throws Exception {
+ AccountGroup.UUID uuid1 = new AccountGroup.UUID("abcd1");
+ AccountGroup.UUID uuid2 = new AccountGroup.UUID("abcd2");
+ AccountGroup.UUID uuid3 = new AccountGroup.UUID("abcd3");
+ Project.NameKey allUsers = new Project.NameKey(AllUsersNameProvider.DEFAULT);
+ Repository allUsersRepo = repoManager.createRepository(allUsers);
+
+ GroupCache cache = EasyMock.createNiceMock(GroupCache.class);
+ GroupIndexer indexer = EasyMock.createNiceMock(GroupIndexer.class);
+ GroupIncludeCache includeCache = EasyMock.createNiceMock(GroupIncludeCache.class);
+ cache.evict(uuid2);
+ indexer.index(uuid2);
+ includeCache.evictParentGroupsOf(uuid2);
+ cache.evict(uuid3);
+ indexer.index(uuid3);
+ includeCache.evictParentGroupsOf(uuid3);
+ EasyMock.expectLastCall();
+
+ EasyMock.replay(cache, indexer);
+
+ Ref ref1 = createRef(allUsersRepo, RefNames.refsGroups(uuid1));
+ Ref ref2 = createRef(allUsersRepo, RefNames.refsGroups(uuid2));
+ try (ProjectResetter resetProject =
+ builder(null, null, null, cache, includeCache, indexer, null)
+ .build(new ProjectResetter.Config().reset(project).reset(allUsers))) {
+ updateRef(allUsersRepo, ref2);
+ createRef(allUsersRepo, RefNames.refsGroups(uuid3));
+ }
+
+ EasyMock.verify(cache, indexer);
+ }
+
private Ref createRef(String ref) throws IOException {
return createRef(repo, ref);
}
@@ -474,13 +511,16 @@
}
private ProjectResetter.Builder builder() {
- return builder(null, null, null, null);
+ return builder(null, null, null, null, null, null, null);
}
private ProjectResetter.Builder builder(
@Nullable AccountCreator accountCreator,
@Nullable AccountCache accountCache,
@Nullable AccountIndexer accountIndexer,
+ @Nullable GroupCache groupCache,
+ @Nullable GroupIncludeCache groupIncludeCache,
+ @Nullable GroupIndexer groupIndexer,
@Nullable ProjectCache projectCache) {
return new ProjectResetter.Builder(
repoManager,
@@ -488,6 +528,9 @@
accountCreator,
accountCache,
accountIndexer,
+ groupCache,
+ groupIncludeCache,
+ groupIndexer,
projectCache);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/TestGroupBackendTest.java b/javatests/com/google/gerrit/acceptance/TestGroupBackendTest.java
new file mode 100644
index 0000000..3c7b966
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/TestGroupBackendTest.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.UniversalGroupBackend;
+import com.google.gerrit.server.group.testing.TestGroupBackend;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+public class TestGroupBackendTest extends AbstractDaemonTest {
+ @Inject private DynamicSet<GroupBackend> groupBackends;
+ @Inject private UniversalGroupBackend universalGroupBackend;
+
+ private final TestGroupBackend testGroupBackend = new TestGroupBackend();
+ private final AccountGroup.UUID testUUID = new AccountGroup.UUID("testbackend:test");
+
+ @Test
+ public void handlesTestGroup() throws Exception {
+ assertThat(testGroupBackend.handles(testUUID)).isTrue();
+ }
+
+ @Test
+ public void universalGroupBackendHandlesTestGroup() throws Exception {
+ RegistrationHandle registrationHandle = groupBackends.add(testGroupBackend);
+ try {
+ assertThat(universalGroupBackend.handles(testUUID)).isTrue();
+ } finally {
+ registrationHandle.remove();
+ }
+ }
+
+ @Test
+ public void doesNotHandleLDAP() throws Exception {
+ assertThat(testGroupBackend.handles(new AccountGroup.UUID("ldap:1234"))).isFalse();
+ }
+
+ @Test
+ public void doesNotHandleNull() throws Exception {
+ assertThat(testGroupBackend.handles(null)).isFalse();
+ }
+
+ @Test
+ public void returnsNullWhenGroupDoesNotExist() throws Exception {
+ assertThat(testGroupBackend.get(testUUID)).isNull();
+ }
+
+ @Test
+ public void returnsNullForNullGroup() throws Exception {
+ assertThat(testGroupBackend.get(null)).isNull();
+ }
+
+ @Test
+ public void returnsKnownGroup() throws Exception {
+ testGroupBackend.create(testUUID);
+ assertThat(testGroupBackend.get(testUUID)).isNotNull();
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index b1606ee..e88e662 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -33,7 +33,6 @@
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
-import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -41,23 +40,27 @@
import com.google.common.cache.LoadingCache;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.io.BaseEncoding;
+import com.google.common.truth.Correspondence;
import com.google.common.util.concurrent.AtomicLongMap;
import com.google.common.util.concurrent.Runnables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.AccountCreator;
import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
+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.common.data.PermissionRule.Action;
import com.google.gerrit.extensions.api.accounts.AccountInput;
import com.google.gerrit.extensions.api.accounts.EmailInput;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
@@ -71,6 +74,7 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.EmailInfo;
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.AccountIndexedListener;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
@@ -97,6 +101,7 @@
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.account.ProjectWatches;
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
+import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdNotes;
import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -119,6 +124,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
+import com.jcraft.jsch.KeyPair;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
@@ -130,6 +136,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -201,10 +208,14 @@
@Inject private ExternalIdNotes.Factory extIdNotesFactory;
+ @Inject private VersionedAuthorizedKeys.Accessor authorizedKeys;
+
@Inject
@Named("accounts")
private LoadingCache<Account.Id, Optional<AccountState>> accountsCache;
+ @Inject private AccountOperations accountOperations;
+
private AccountIndexedCounter accountIndexedCounter;
private RegistrationHandle accountIndexEventCounterHandle;
private RefUpdateCounter refUpdateCounter;
@@ -262,6 +273,26 @@
}
}
+ protected void assertLabelPermission(
+ Project.NameKey project,
+ GroupReference groupReference,
+ String ref,
+ boolean exclusive,
+ String labelName,
+ int min,
+ int max)
+ throws IOException {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ AccessSection accessSection = cfg.getAccessSection(ref);
+ assertThat(accessSection).isNotNull();
+
+ String permissionName = Permission.LABEL + labelName;
+ Permission permission = accessSection.getPermission(permissionName);
+ assertPermission(permission, permissionName, exclusive, labelName);
+ assertPermissionRule(
+ permission.getRule(groupReference), groupReference, Action.ALLOW, false, min, max);
+ }
+
@Test
public void createByAccountCreator() throws Exception {
Account.Id accountId = createByAccountCreator(2); // account creation + external ID creation
@@ -271,22 +302,6 @@
RefUpdateCounter.projectRef(allUsers, RefNames.REFS_SEQUENCES + Sequences.NAME_ACCOUNTS));
}
- @Test
- @UseSsh
- public void createWithSshKeysByAccountCreator() throws Exception {
- Account.Id accountId =
- createByAccountCreator(3); // account creation + external ID creation + adding SSH keys
- refUpdateCounter.assertRefUpdateFor(
- ImmutableMap.of(
- RefUpdateCounter.projectRef(allUsers, RefNames.refsUsers(accountId)),
- 2,
- RefUpdateCounter.projectRef(allUsers, RefNames.REFS_EXTERNAL_IDS),
- 1,
- RefUpdateCounter.projectRef(
- allUsers, RefNames.REFS_SEQUENCES + Sequences.NAME_ACCOUNTS),
- 1));
- }
-
private Account.Id createByAccountCreator(int expectedAccountReindexCalls) throws Exception {
String name = "foo";
TestAccount foo = accountCreator.create(name);
@@ -1018,10 +1033,12 @@
String userRefName = RefNames.refsUsers(user.id);
// remove default READ permissions
- ProjectConfig cfg = projectCache.checkedGet(allUsers).getConfig();
- cfg.getAccessSection(RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", true)
- .remove(new Permission(Permission.READ));
- saveProjectConfig(allUsers, cfg);
+ try (ProjectConfigUpdate u = updateProject(allUsers)) {
+ u.getConfig()
+ .getAccessSection(RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", true)
+ .remove(new Permission(Permission.READ));
+ u.save();
+ }
// deny READ permission that is inherited from All-Projects
deny(allUsers, RefNames.REFS + "*", Permission.READ, ANONYMOUS_USERS);
@@ -1825,18 +1842,18 @@
@Test
@UseSsh
public void sshKeys() throws Exception {
- //
// The test account should initially have exactly one ssh key
List<SshKeyInfo> info = gApi.accounts().self().listSshKeys();
assertThat(info).hasSize(1);
assertSequenceNumbers(info);
SshKeyInfo key = info.get(0);
- String inital = AccountCreator.publicKey(admin.sshKey, admin.email);
+ KeyPair keyPair = sshKeys.getKeyPair(admin);
+ String inital = TestSshKeys.publicKey(keyPair, admin.email);
assertThat(key.sshPublicKey).isEqualTo(inital);
accountIndexedCounter.assertNoReindex();
// Add a new key
- String newKey = AccountCreator.publicKey(AccountCreator.genSshKey(), admin.email);
+ String newKey = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email);
gApi.accounts().self().addSshKey(newKey);
info = gApi.accounts().self().listSshKeys();
assertThat(info).hasSize(2);
@@ -1851,7 +1868,7 @@
accountIndexedCounter.assertNoReindex();
// Add another new key
- String newKey2 = AccountCreator.publicKey(AccountCreator.genSshKey(), admin.email);
+ String newKey2 = TestSshKeys.publicKey(TestSshKeys.genSshKey(), admin.email);
gApi.accounts().self().addSshKey(newKey2);
info = gApi.accounts().self().listSshKeys();
assertThat(info).hasSize(3);
@@ -1865,6 +1882,16 @@
assertThat(info.get(0).seq).isEqualTo(1);
assertThat(info.get(1).seq).isEqualTo(3);
accountIndexedCounter.assertReindexOf(admin);
+
+ // Mark first key as invalid
+ assertThat(info.get(0).valid).isTrue();
+ authorizedKeys.markKeyInvalid(admin.id, 1);
+ info = gApi.accounts().self().listSshKeys();
+ assertThat(info).hasSize(2);
+ assertThat(info.get(0).seq).isEqualTo(1);
+ assertThat(info.get(0).valid).isFalse();
+ assertThat(info.get(1).seq).isEqualTo(3);
+ accountIndexedCounter.assertReindexOf(admin);
}
// reindex is tested by {@link AbstractQueryAccountsTest#reindex}
@@ -1982,15 +2009,24 @@
}
@Test
- public void groups() throws Exception {
- assertGroups(
- admin.username, ImmutableList.of("Anonymous Users", "Registered Users", "Administrators"));
+ public void allGroupsForAnAdminAccountCanBeRetrieved() throws Exception {
+ List<GroupInfo> groups = gApi.accounts().id(admin.username).getGroups();
+ assertThat(groups)
+ .comparingElementsUsing(getGroupToNameCorrespondence())
+ .containsExactly("Anonymous Users", "Registered Users", "Administrators");
+ }
- assertGroups(user.username, ImmutableList.of("Anonymous Users", "Registered Users"));
-
+ @Test
+ public void allGroupsForAUserAccountCanBeRetrieved() throws Exception {
+ String username = name("user1");
+ accountOperations.newAccount().username(username).create();
String group = createGroup("group");
- String newUser = createAccount("user1", group);
- assertGroups(newUser, ImmutableList.of("Anonymous Users", "Registered Users", group));
+ gApi.groups().id(group).addMembers(username);
+
+ List<GroupInfo> allGroups = gApi.accounts().id(username).getGroups();
+ assertThat(allGroups)
+ .comparingElementsUsing(getGroupToNameCorrespondence())
+ .containsExactly("Anonymous Users", "Registered Users", group);
}
@Test
@@ -2368,13 +2404,19 @@
assertThat(stalenessChecker.isStale(accountId)).isFalse();
}
- private void assertGroups(String user, List<String> expected) throws Exception {
- List<String> actual = getNamesOfGroupsOfUser(user);
- assertThat(actual).containsExactlyElementsIn(expected);
- }
+ private static Correspondence<GroupInfo, String> getGroupToNameCorrespondence() {
+ return new Correspondence<GroupInfo, String>() {
+ @Override
+ public boolean compare(GroupInfo actualGroup, String expectedName) {
+ String groupName = actualGroup == null ? null : actualGroup.name;
+ return Objects.equals(groupName, expectedName);
+ }
- private List<String> getNamesOfGroupsOfUser(String user) throws RestApiException {
- return gApi.accounts().id(user).getGroups().stream().map(g -> g.name).collect(toList());
+ @Override
+ public String toString() {
+ return "has name";
+ }
+ };
}
private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index 607d7d0..ed5459d 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -183,9 +183,9 @@
AuthResult authResult = accountManager.authenticate(who);
assertAuthResultForExistingAccount(authResult, accountId, gerritExtIdKey);
- ExternalId gerritExtId = externalIds.get(gerritExtIdKey);
- assertThat(gerritExtId).isNotNull();
- assertThat(gerritExtId.email()).isEqualTo(newEmail);
+ Optional<ExternalId> gerritExtId = externalIds.get(gerritExtIdKey);
+ assertThat(gerritExtId).isPresent();
+ assertThat(gerritExtId.get().email()).isEqualTo(newEmail);
Optional<AccountState> accountState = accounts.get(accountId);
assertThat(accountState).isPresent();
@@ -420,9 +420,9 @@
}
// Verify that the email in the external ID was not updated.
- ExternalId gerritExtId = externalIds.get(gerritExtIdKey);
- assertThat(gerritExtId).isNotNull();
- assertThat(gerritExtId.email()).isEqualTo(email);
+ Optional<ExternalId> gerritExtId = externalIds.get(gerritExtIdKey);
+ assertThat(gerritExtId).isPresent();
+ assertThat(gerritExtId.get().email()).isEqualTo(email);
// Verify that the preferred email was not updated.
Optional<AccountState> accountState = accounts.get(accountId);
@@ -537,7 +537,7 @@
private void assertNoSuchExternalIds(ExternalId.Key... extIdKeys) throws Exception {
for (ExternalId.Key extIdKey : extIdKeys) {
- assertThat(externalIds.get(extIdKey)).named(extIdKey.get()).isNull();
+ assertThat(externalIds.get(extIdKey)).named(extIdKey.get()).isEmpty();
}
}
@@ -557,14 +557,14 @@
@Nullable Account.Id expectedAccountId,
@Nullable String expectedEmail)
throws Exception {
- ExternalId extId = externalIds.get(extIdKey);
- assertThat(extId).named(extIdKey.get()).isNotNull();
+ Optional<ExternalId> extId = externalIds.get(extIdKey);
+ assertThat(extId).named(extIdKey.get()).isPresent();
if (expectedAccountId != null) {
- assertThat(extId.accountId())
+ assertThat(extId.get().accountId())
.named("account ID of " + extIdKey.get())
.isEqualTo(expectedAccountId);
}
- assertThat(extId.email()).named("email of " + extIdKey.get()).isEqualTo(expectedEmail);
+ assertThat(extId.get().email()).named("email of " + extIdKey.get()).isEqualTo(expectedEmail);
}
private void assertAuthResultForNewAccount(
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 57a9744..cd2dec7 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -65,8 +65,9 @@
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
-import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.LabelFunction;
@@ -87,6 +88,7 @@
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.changes.StarsInput;
import com.google.gerrit.extensions.api.groups.GroupApi;
+import com.google.gerrit.extensions.api.projects.BranchApi;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.client.ChangeKind;
@@ -135,8 +137,8 @@
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.git.ChangeMessageModifier;
import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import com.google.gerrit.server.restapi.change.PostReview;
import com.google.gerrit.server.update.BatchUpdate;
@@ -179,6 +181,8 @@
@Inject private DynamicSet<ChangeIndexedListener> changeIndexedListeners;
+ @Inject private AccountOperations accountOperations;
+
private ChangeIndexedCounter changeIndexedCounter;
private RegistrationHandle changeIndexedCounterHandle;
@@ -477,20 +481,40 @@
assertThat(gApi.changes().id(changeId).get().pendingReviewers).isEmpty();
// Add some pending reviewers.
- TestAccount user1 =
- accountCreator.create(name("user1"), name("user1") + "@example.com", "User 1");
- TestAccount user2 =
- accountCreator.create(name("user2"), name("user2") + "@example.com", "User 2");
- TestAccount user3 =
- accountCreator.create(name("user3"), name("user3") + "@example.com", "User 3");
- TestAccount user4 =
- accountCreator.create(name("user4"), name("user4") + "@example.com", "User 4");
+ String email1 = name("user1") + "@example.com";
+ String email2 = name("user2") + "@example.com";
+ String email3 = name("user3") + "@example.com";
+ String email4 = name("user4") + "@example.com";
+ accountOperations
+ .newAccount()
+ .username(name("user1"))
+ .preferredEmail(email1)
+ .fullname("User 1")
+ .create();
+ accountOperations
+ .newAccount()
+ .username(name("user2"))
+ .preferredEmail(email2)
+ .fullname("User 2")
+ .create();
+ accountOperations
+ .newAccount()
+ .username(name("user3"))
+ .preferredEmail(email3)
+ .fullname("User 3")
+ .create();
+ accountOperations
+ .newAccount()
+ .username(name("user4"))
+ .preferredEmail(email4)
+ .fullname("User 4")
+ .create();
ReviewInput in =
ReviewInput.noScore()
- .reviewer(user1.email)
- .reviewer(user2.email)
- .reviewer(user3.email, CC, false)
- .reviewer(user4.email, CC, false)
+ .reviewer(email1)
+ .reviewer(email2)
+ .reviewer(email3, CC, false)
+ .reviewer(email4, CC, false)
.reviewer("byemail1@example.com")
.reviewer("byemail2@example.com")
.reviewer("byemail3@example.com", CC, false)
@@ -502,43 +526,43 @@
ais -> ais.stream().map(ai -> ai.email).collect(toSet());
assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
.containsExactly(
- admin.email, user1.email, user2.email, "byemail1@example.com", "byemail2@example.com");
+ admin.email, email1, email2, "byemail1@example.com", "byemail2@example.com");
assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
- .containsExactly(user3.email, user4.email, "byemail3@example.com", "byemail4@example.com");
+ .containsExactly(email3, email4, "byemail3@example.com", "byemail4@example.com");
assertThat(info.pendingReviewers.get(REMOVED)).isNull();
// Stage some pending reviewer removals.
- gApi.changes().id(changeId).reviewer(user1.email).remove();
- gApi.changes().id(changeId).reviewer(user3.email).remove();
+ gApi.changes().id(changeId).reviewer(email1).remove();
+ gApi.changes().id(changeId).reviewer(email3).remove();
gApi.changes().id(changeId).reviewer("byemail1@example.com").remove();
gApi.changes().id(changeId).reviewer("byemail3@example.com").remove();
info = gApi.changes().id(changeId).get();
assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
- .containsExactly(admin.email, user2.email, "byemail2@example.com");
+ .containsExactly(admin.email, email2, "byemail2@example.com");
assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
- .containsExactly(user4.email, "byemail4@example.com");
+ .containsExactly(email4, "byemail4@example.com");
assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED)))
- .containsExactly(user1.email, user3.email, "byemail1@example.com", "byemail3@example.com");
+ .containsExactly(email1, email3, "byemail1@example.com", "byemail3@example.com");
// "Undo" a removal.
- in = ReviewInput.noScore().reviewer(user1.email);
+ in = ReviewInput.noScore().reviewer(email1);
gApi.changes().id(changeId).revision("current").review(in);
info = gApi.changes().id(changeId).get();
assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER)))
- .containsExactly(admin.email, user1.email, user2.email, "byemail2@example.com");
+ .containsExactly(admin.email, email1, email2, "byemail2@example.com");
assertThat(toEmails.apply(info.pendingReviewers.get(CC)))
- .containsExactly(user4.email, "byemail4@example.com");
+ .containsExactly(email4, "byemail4@example.com");
assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED)))
- .containsExactly(user3.email, "byemail1@example.com", "byemail3@example.com");
+ .containsExactly(email3, "byemail1@example.com", "byemail3@example.com");
// "Commit" by moving out of WIP.
gApi.changes().id(changeId).setReadyForReview();
info = gApi.changes().id(changeId).get();
assertThat(info.pendingReviewers).isEmpty();
assertThat(toEmails.apply(info.reviewers.get(REVIEWER)))
- .containsExactly(admin.email, user1.email, user2.email, "byemail2@example.com");
+ .containsExactly(admin.email, email1, email2, "byemail2@example.com");
assertThat(toEmails.apply(info.reviewers.get(CC)))
- .containsExactly(user4.email, "byemail4@example.com");
+ .containsExactly(email4, "byemail4@example.com");
assertThat(info.reviewers.get(REMOVED)).isNull();
}
@@ -1315,10 +1339,11 @@
public void pushCommitOfOtherUserThatCannotSeeChange() throws Exception {
// create hidden project that is only visible to administrators
Project.NameKey p = createProject("p");
- ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
- Util.allow(cfg, Permission.READ, adminGroupUuid(), "refs/*");
- Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
- saveProjectConfig(p, cfg);
+ try (ProjectConfigUpdate u = updateProject(p)) {
+ Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
+ Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
+ u.save();
+ }
// admin pushes commit of user
TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
@@ -1388,10 +1413,11 @@
public void pushCommitWithFooterOfOtherUserThatCannotSeeChange() throws Exception {
// create hidden project that is only visible to administrators
Project.NameKey p = createProject("p");
- ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
- Util.allow(cfg, Permission.READ, adminGroupUuid(), "refs/*");
- Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
- saveProjectConfig(p, cfg);
+ try (ProjectConfigUpdate u = updateProject(p)) {
+ Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
+ Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
+ u.save();
+ }
// admin pushes commit that references 'user' in a footer
TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
@@ -1431,10 +1457,11 @@
public void addReviewerThatCannotSeeChange() throws Exception {
// create hidden project that is only visible to administrators
Project.NameKey p = createProject("p");
- ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
- Util.allow(cfg, Permission.READ, adminGroupUuid(), "refs/*");
- Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
- saveProjectConfig(p, cfg);
+ try (ProjectConfigUpdate u = updateProject(p)) {
+ Util.allow(u.getConfig(), Permission.READ, adminGroupUuid(), "refs/*");
+ Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
+ u.save();
+ }
// create change
TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
@@ -1541,7 +1568,7 @@
// Change status of reviewer and ensure ETag is updated.
oldETag = rsrc.getETag();
- gApi.accounts().id(user.id.get()).setStatus("new status");
+ accountOperations.account(user.id).forUpdate().status("new status").update();
rsrc = parseResource(r);
assertThat(rsrc.getETag()).isNotEqualTo(oldETag);
}
@@ -1588,8 +1615,16 @@
String oldETag = rsrc.getETag();
Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
- //create a group named "ab" with one user: testUser
- TestAccount testUser = accountCreator.create("abcd", "abcd@test.com", "abcd");
+ // create a group named "ab" with one user: testUser
+ String email = "abcd@test.com";
+ String fullname = "abcd";
+ TestAccount testUser =
+ accountOperations
+ .newAccount()
+ .username("abcd")
+ .preferredEmail(email)
+ .fullname(fullname)
+ .create();
String testGroup = createGroupWithRealName("ab");
GroupApi groupApi = gApi.groups().id(testGroup);
groupApi.description("test group");
@@ -1602,11 +1637,11 @@
List<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
- assertThat(m.rcpt()).containsExactly(testUser.emailAddress);
- assertThat(m.body()).contains("Hello " + testUser.fullName + ",\n");
+ assertThat(m.rcpt()).containsExactly(new Address(fullname, email));
+ assertThat(m.body()).contains("Hello " + fullname + ",\n");
assertThat(m.body()).contains("I'd like you to do a code review.");
assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
- assertMailReplyTo(m, testUser.email);
+ assertMailReplyTo(m, email);
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
// When NoteDb is enabled adding a reviewer records that user as reviewer
@@ -1616,7 +1651,7 @@
Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
assertThat(reviewers).isNotNull();
assertThat(reviewers).hasSize(1);
- assertThat(reviewers.iterator().next()._accountId).isEqualTo(testUser.getId().get());
+ assertThat(reviewers.iterator().next()._accountId).isEqualTo(testUser.accountId().get());
// Ensure ETag and lastUpdatedOn are updated.
rsrc = parseResource(r);
@@ -1632,17 +1667,32 @@
String oldETag = rsrc.getETag();
Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
- //create a group named "kobe" with one user: lee
- TestAccount testUser = accountCreator.create("kobebryant", "kobebryant@test.com", "kobebryant");
- TestAccount myGroupUser = accountCreator.create("lee", "lee@test.com", "lee");
+ // create a group named "kobe" with one user: lee
+ String testUserFullname = "kobebryant";
+ accountOperations
+ .newAccount()
+ .username("kobebryant")
+ .preferredEmail("kobebryant@test.com")
+ .fullname(testUserFullname)
+ .create();
+
+ String myGroupUserEmail = "lee@test.com";
+ String myGroupUserFullname = "lee";
+ TestAccount myGroupUser =
+ accountOperations
+ .newAccount()
+ .username("lee")
+ .preferredEmail(myGroupUserEmail)
+ .fullname(myGroupUserFullname)
+ .create();
String testGroup = createGroupWithRealName("kobe");
GroupApi groupApi = gApi.groups().id(testGroup);
groupApi.description("test group");
- groupApi.addMembers(myGroupUser.fullName);
+ groupApi.addMembers(myGroupUserFullname);
- //ensure that user "user" is not in the group
- groupApi.removeMembers(testUser.fullName);
+ // ensure that user "user" is not in the group
+ groupApi.removeMembers(testUserFullname);
AddReviewerInput in = new AddReviewerInput();
in.reviewer = testGroup;
@@ -1651,11 +1701,11 @@
List<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
Message m = messages.get(0);
- assertThat(m.rcpt()).containsExactly(myGroupUser.emailAddress);
- assertThat(m.body()).contains("Hello " + myGroupUser.fullName + ",\n");
+ assertThat(m.rcpt()).containsExactly(new Address(myGroupUserFullname, myGroupUserEmail));
+ assertThat(m.body()).contains("Hello " + myGroupUserFullname + ",\n");
assertThat(m.body()).contains("I'd like you to do a code review.");
assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n");
- assertMailReplyTo(m, myGroupUser.email);
+ assertMailReplyTo(m, myGroupUserEmail);
ChangeInfo c = gApi.changes().id(r.getChangeId()).get();
// When NoteDb is enabled adding a reviewer records that user as reviewer
@@ -1665,7 +1715,7 @@
Collection<AccountInfo> reviewers = c.reviewers.get(REVIEWER);
assertThat(reviewers).isNotNull();
assertThat(reviewers).hasSize(1);
- assertThat(reviewers.iterator().next()._accountId).isEqualTo(myGroupUser.getId().get());
+ assertThat(reviewers.iterator().next()._accountId).isEqualTo(myGroupUser.accountId().get());
// Ensure ETag and lastUpdatedOn are updated.
rsrc = parseResource(r);
@@ -1732,12 +1782,13 @@
@Test
public void implicitlyCcOnNonVotingReviewForUserWithoutUserNamePgStyle() throws Exception {
- TestAccount accountWithoutUsername = accountCreator.create();
+ com.google.gerrit.acceptance.TestAccount accountWithoutUsername = accountCreator.create();
assertThat(accountWithoutUsername.username).isNull();
testImplicitlyCcOnNonVotingReviewPgStyle(accountWithoutUsername);
}
- private void testImplicitlyCcOnNonVotingReviewPgStyle(TestAccount testAccount) throws Exception {
+ private void testImplicitlyCcOnNonVotingReviewPgStyle(
+ com.google.gerrit.acceptance.TestAccount testAccount) throws Exception {
PushOneCommit.Result r = createChange();
setApiUser(testAccount);
assertThat(getReviewerState(r.getChangeId(), testAccount.id)).isEmpty();
@@ -1762,12 +1813,13 @@
@Test
public void implicitlyCcOnNonVotingReviewForUserWithoutUserNameGwtStyle() throws Exception {
- TestAccount accountWithoutUsername = accountCreator.create();
+ com.google.gerrit.acceptance.TestAccount accountWithoutUsername = accountCreator.create();
assertThat(accountWithoutUsername.username).isNull();
testImplicitlyCcOnNonVotingReviewGwtStyle(accountWithoutUsername);
}
- private void testImplicitlyCcOnNonVotingReviewGwtStyle(TestAccount testAccount) throws Exception {
+ private void testImplicitlyCcOnNonVotingReviewGwtStyle(
+ com.google.gerrit.acceptance.TestAccount testAccount) throws Exception {
PushOneCommit.Result r = createChange();
setApiUser(testAccount);
assertThat(getReviewerState(r.getChangeId(), testAccount.id)).isEmpty();
@@ -1918,16 +1970,21 @@
@Test
public void removeReviewerNoVotes() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
-
- LabelType verified =
- category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
- cfg.getLabelSections().put(verified.getName(), verified);
-
- AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
- String heads = RefNames.REFS_HEADS + "*";
- Util.allow(cfg, Permission.forLabel(Util.verified().getName()), -1, 1, registeredUsers, heads);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType verified =
+ category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+ u.getConfig().getLabelSections().put(verified.getName(), verified);
+ AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+ String heads = RefNames.REFS_HEADS + "*";
+ Util.allow(
+ u.getConfig(),
+ Permission.forLabel(Util.verified().getName()),
+ -1,
+ 1,
+ registeredUsers,
+ heads);
+ u.save();
+ }
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
@@ -2087,13 +2144,20 @@
in.notify = NotifyHandling.NONE;
// notify unrelated account as TO
- TestAccount user2 = accountCreator.user2();
+ String email = "user2@example.com";
+ TestAccount user2 =
+ accountOperations
+ .newAccount()
+ .username("user2")
+ .preferredEmail(email)
+ .fullname("User2")
+ .create();
setApiUser(user);
recommend(r.getChangeId());
setApiUser(admin);
sender.clear();
in.notifyDetails = new HashMap<>();
- in.notifyDetails.put(RecipientType.TO, new NotifyInfo(ImmutableList.of(user2.email)));
+ in.notifyDetails.put(RecipientType.TO, new NotifyInfo(ImmutableList.of(email)));
gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
assertNotifyTo(user2);
@@ -2103,7 +2167,7 @@
setApiUser(admin);
sender.clear();
in.notifyDetails = new HashMap<>();
- in.notifyDetails.put(RecipientType.CC, new NotifyInfo(ImmutableList.of(user2.email)));
+ in.notifyDetails.put(RecipientType.CC, new NotifyInfo(ImmutableList.of(email)));
gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
assertNotifyCc(user2);
@@ -2113,7 +2177,7 @@
setApiUser(admin);
sender.clear();
in.notifyDetails = new HashMap<>();
- in.notifyDetails.put(RecipientType.BCC, new NotifyInfo(ImmutableList.of(user2.email)));
+ in.notifyDetails.put(RecipientType.BCC, new NotifyInfo(ImmutableList.of(email)));
gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).deleteVote(in);
assertNotifyBcc(user2);
}
@@ -2133,14 +2197,15 @@
public void nonVotingReviewerStaysAfterSubmit() throws Exception {
LabelType verified =
category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().put(verified.getName(), verified);
- String heads = "refs/heads/*";
- AccountGroup.UUID owners = systemGroupBackend.getGroup(CHANGE_OWNER).getUUID();
- AccountGroup.UUID registered = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
- Util.allow(cfg, Permission.forLabel(verified.getName()), -1, 1, owners, heads);
- Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2, registered, heads);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().put(verified.getName(), verified);
+ String heads = "refs/heads/*";
+ AccountGroup.UUID owners = systemGroupBackend.getGroup(CHANGE_OWNER).getUUID();
+ AccountGroup.UUID registered = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+ Util.allow(u.getConfig(), Permission.forLabel(verified.getName()), -1, 1, owners, heads);
+ Util.allow(u.getConfig(), Permission.forLabel("Code-Review"), -2, +2, registered, heads);
+ u.save();
+ }
// Set Code-Review+2 and Verified+1 as admin (change owner)
PushOneCommit.Result r = createChange();
@@ -2406,16 +2471,17 @@
category("Custom1", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
LabelType custom2 =
category("Custom2", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().put(verified.getName(), verified);
- cfg.getLabelSections().put(custom1.getName(), custom1);
- cfg.getLabelSections().put(custom2.getName(), custom2);
- String heads = "refs/heads/*";
- AccountGroup.UUID anon = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
- Util.allow(cfg, Permission.forLabel("Verified"), -1, 1, anon, heads);
- Util.allow(cfg, Permission.forLabel("Custom1"), -1, 1, anon, heads);
- Util.allow(cfg, Permission.forLabel("Custom2"), -1, 1, anon, heads);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().put(verified.getName(), verified);
+ u.getConfig().getLabelSections().put(custom1.getName(), custom1);
+ u.getConfig().getLabelSections().put(custom2.getName(), custom2);
+ String heads = "refs/heads/*";
+ AccountGroup.UUID anon = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
+ Util.allow(u.getConfig(), Permission.forLabel("Verified"), -1, 1, anon, heads);
+ Util.allow(u.getConfig(), Permission.forLabel("Custom1"), -1, 1, anon, heads);
+ Util.allow(u.getConfig(), Permission.forLabel("Custom2"), -1, 1, anon, heads);
+ u.save();
+ }
PushOneCommit.Result r1 = createChange();
r1.assertOkStatus();
@@ -2538,9 +2604,11 @@
assertThat(approval._accountId).isEqualTo(user.id.get());
assertThat(approval.value).isEqualTo(0);
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.blockLabel(cfg, "Code-Review", REGISTERED_USERS, "refs/heads/*");
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.blockLabel(u.getConfig(), "Code-Review", REGISTERED_USERS, "refs/heads/*");
+ u.save();
+ }
+
c = gApi.changes().id(triplet).get(DETAILED_LABELS);
codeReview = c.labels.get("Code-Review");
assertThat(codeReview.all).hasSize(1);
@@ -2869,13 +2937,16 @@
assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review");
// add new label and assert that it's returned for existing changes
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- LabelType verified = Util.verified();
- cfg.getLabelSections().put(verified.getName(), verified);
AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+ LabelType verified = Util.verified();
String heads = RefNames.REFS_HEADS + "*";
- Util.allow(cfg, Permission.forLabel(verified.getName()), -1, 1, registeredUsers, heads);
- saveProjectConfig(project, cfg);
+
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().put(verified.getName(), verified);
+ Util.allow(
+ u.getConfig(), Permission.forLabel(verified.getName()), -1, 1, registeredUsers, heads);
+ u.save();
+ }
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.labels.keySet()).containsExactly("Code-Review", "Verified");
@@ -2889,11 +2960,13 @@
.revision(r.getCommit().name())
.review(new ReviewInput().label(verified.getName(), verified.getMax().getValue()));
- // remove label and assert that it's no longer returned for existing
- // changes, even if there is an approval for it
- cfg.getLabelSections().remove(verified.getName());
- Util.remove(cfg, Permission.forLabel(verified.getName()), registeredUsers, heads);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ // remove label and assert that it's no longer returned for existing
+ // changes, even if there is an approval for it
+ u.getConfig().getLabelSections().remove(verified.getName());
+ Util.remove(u.getConfig(), Permission.forLabel(verified.getName()), registeredUsers, heads);
+ u.save();
+ }
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.labels.keySet()).containsExactly("Code-Review");
@@ -2920,14 +2993,17 @@
assertThat(change.permittedLabels.keySet()).containsExactly("Code-Review");
assertPermitted(change, "Code-Review", 2);
- // add new label and assert that it's returned for existing changes
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
LabelType verified = Util.verified();
- cfg.getLabelSections().put(verified.getName(), verified);
AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
String heads = RefNames.REFS_HEADS + "*";
- Util.allow(cfg, Permission.forLabel(verified.getName()), -1, 1, registeredUsers, heads);
- saveProjectConfig(project, cfg);
+
+ // add new label and assert that it's returned for existing changes
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().put(verified.getName(), verified);
+ Util.allow(
+ u.getConfig(), Permission.forLabel(verified.getName()), -1, 1, registeredUsers, heads);
+ u.save();
+ }
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.labels.keySet()).containsExactly("Code-Review", "Verified");
@@ -2968,10 +3044,11 @@
// remove label and assert that it's no longer returned for existing
// changes, even if there is an approval for it
- cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().remove(verified.getName());
- Util.remove(cfg, Permission.forLabel(verified.getName()), registeredUsers, heads);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().remove(verified.getName());
+ Util.remove(u.getConfig(), Permission.forLabel(verified.getName()), registeredUsers, heads);
+ u.save();
+ }
change = gApi.changes().id(r.getChangeId()).get();
assertThat(change.labels.keySet()).containsExactly("Code-Review");
@@ -3008,13 +3085,20 @@
push2.to(RefNames.REFS_CONFIG);
testRepo.reset(oldHead);
- // Allow user to approve
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
String heads = RefNames.REFS_HEADS + "*";
- Util.allow(
- cfg, Permission.forLabel(Util.codeReview().getName()), -2, 2, registeredUsers, heads);
- saveProjectConfig(project, cfg);
+
+ // Allow user to approve
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.allow(
+ u.getConfig(),
+ Permission.forLabel(Util.codeReview().getName()),
+ -2,
+ 2,
+ registeredUsers,
+ heads);
+ u.save();
+ }
PushOneCommit.Result r = createChange();
@@ -3066,15 +3150,16 @@
assertThat(approval.permittedVotingRange.min).isEqualTo(-1);
assertThat(approval.permittedVotingRange.max).isEqualTo(1);
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.allow(
- cfg,
- Permission.forLabel("Code-Review"),
- minPermittedValue,
- maxPermittedValue,
- REGISTERED_USERS,
- heads);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.allow(
+ u.getConfig(),
+ Permission.forLabel("Code-Review"),
+ minPermittedValue,
+ maxPermittedValue,
+ REGISTERED_USERS,
+ heads);
+ u.save();
+ }
c = gApi.changes().id(triplet).get(DETAILED_LABELS);
codeReview = c.labels.get("Code-Review");
@@ -3088,9 +3173,10 @@
@Test
public void maxPermittedValueBlocked() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.blockLabel(cfg, "Code-Review", REGISTERED_USERS, "refs/heads/*");
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.blockLabel(u.getConfig(), "Code-Review", REGISTERED_USERS, "refs/heads/*");
+ u.save();
+ }
PushOneCommit.Result r = createChange();
String triplet = project.get() + "~master~" + r.getChangeId();
@@ -3106,6 +3192,58 @@
}
@Test
+ public void nonStrictLabelWithInvalidLabelPerDefault() throws Exception {
+ String changeId = createChange().getChangeId();
+
+ // Add a review with invalid labels.
+ ReviewInput input = ReviewInput.approve().label("Code-Style", 1);
+ gApi.changes().id(changeId).current().review(input);
+
+ Map<String, Short> votes = gApi.changes().id(changeId).current().reviewer(admin.email).votes();
+ assertThat(votes.keySet()).containsExactly("Code-Review");
+ assertThat(votes.values()).containsExactly((short) 2);
+ }
+
+ @Test
+ public void nonStrictLabelWithInvalidValuePerDefault() throws Exception {
+ String changeId = createChange().getChangeId();
+
+ // Add a review with invalid label values.
+ ReviewInput input = new ReviewInput().label("Code-Review", 3);
+ gApi.changes().id(changeId).current().review(input);
+
+ Map<String, Short> votes = gApi.changes().id(changeId).current().reviewer(admin.email).votes();
+ if (!notesMigration.readChanges()) {
+ assertThat(votes.keySet()).containsExactly("Code-Review");
+ assertThat(votes.values()).containsExactly((short) 0);
+ } else {
+ assertThat(votes).isEmpty();
+ }
+ }
+
+ @Test
+ @GerritConfig(name = "change.strictLabels", value = "true")
+ public void strictLabelWithInvalidLabel() throws Exception {
+ String changeId = createChange().getChangeId();
+ ReviewInput in = new ReviewInput().label("Code-Style", 1);
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("label \"Code-Style\" is not a configured label");
+ gApi.changes().id(changeId).current().review(in);
+ }
+
+ @Test
+ @GerritConfig(name = "change.strictLabels", value = "true")
+ public void strictLabelWithInvalidValue() throws Exception {
+ String changeId = createChange().getChangeId();
+ ReviewInput in = new ReviewInput().label("Code-Review", 3);
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("label \"Code-Review\": 3 is not a valid value");
+ gApi.changes().id(changeId).current().review(in);
+ }
+
+ @Test
public void unresolvedCommentsBlocked() throws Exception {
modifySubmitRules(
"submit_rule(submit(R)) :- \n"
@@ -3194,7 +3332,7 @@
assertThat(getCommitMessage(r.getChangeId()))
.isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n");
- for (TestAccount acc : ImmutableList.of(admin, user)) {
+ for (com.google.gerrit.acceptance.TestAccount acc : ImmutableList.of(admin, user)) {
setApiUser(acc);
String newMessage =
"modified commit by " + acc.username + "\n\nChange-Id: " + r.getChangeId() + "\n";
@@ -3452,11 +3590,13 @@
public void submittableAfterLosingPermissions(String label) throws Exception {
String codeReviewLabel = "Code-Review";
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
AccountGroup.UUID registered = SystemGroupBackend.REGISTERED_USERS;
- Util.allow(cfg, Permission.forLabel(label), -1, +1, registered, "refs/heads/*");
- Util.allow(cfg, Permission.forLabel(codeReviewLabel), -2, +2, registered, "refs/heads/*");
- saveProjectConfig(cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.allow(u.getConfig(), Permission.forLabel(label), -1, +1, registered, "refs/heads/*");
+ Util.allow(
+ u.getConfig(), Permission.forLabel(codeReviewLabel), -2, +2, registered, "refs/heads/*");
+ u.save();
+ }
setApiUser(user);
PushOneCommit.Result r = createChange();
@@ -3480,11 +3620,14 @@
setApiUser(admin);
// Remove user's permission for 'Label'.
- Util.remove(cfg, Permission.forLabel(label), registered, "refs/heads/*");
- // Update user's permitted range for 'Code-Review' to be -1...+1.
- Util.remove(cfg, Permission.forLabel(codeReviewLabel), registered, "refs/heads/*");
- Util.allow(cfg, Permission.forLabel(codeReviewLabel), -1, +1, registered, "refs/heads/*");
- saveProjectConfig(cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.remove(u.getConfig(), Permission.forLabel(label), registered, "refs/heads/*");
+ // Update user's permitted range for 'Code-Review' to be -1...+1.
+ Util.remove(u.getConfig(), Permission.forLabel(codeReviewLabel), registered, "refs/heads/*");
+ Util.allow(
+ u.getConfig(), Permission.forLabel(codeReviewLabel), -1, +1, registered, "refs/heads/*");
+ u.save();
+ }
// Verify user's new permitted range.
setApiUser(user);
@@ -3651,7 +3794,14 @@
@Test
public void ignore() throws Exception {
- TestAccount user2 = accountCreator.user2();
+ String email = "user2@example.com";
+ String fullname = "User2";
+ accountOperations
+ .newAccount()
+ .username("user2")
+ .preferredEmail(email)
+ .fullname(fullname)
+ .create();
PushOneCommit.Result r = createChange();
@@ -3660,7 +3810,7 @@
gApi.changes().id(r.getChangeId()).addReviewer(in);
in = new AddReviewerInput();
- in.reviewer = user2.email;
+ in.reviewer = email;
gApi.changes().id(r.getChangeId()).addReviewer(in);
setApiUser(user);
@@ -3672,7 +3822,7 @@
gApi.changes().id(r.getChangeId()).abandon();
List<Message> messages = sender.getMessages();
assertThat(messages).hasSize(1);
- assertThat(messages.get(0).rcpt()).containsExactly(user2.emailAddress);
+ assertThat(messages.get(0).rcpt()).containsExactly(new Address(fullname, email));
setApiUser(user);
gApi.changes().id(r.getChangeId()).ignore(false);
@@ -3726,7 +3876,7 @@
@Test
public void markAsReviewed() throws Exception {
- TestAccount user2 = accountCreator.user2();
+ com.google.gerrit.acceptance.TestAccount user2 = accountCreator.user2();
PushOneCommit.Result r = createChange();
@@ -3874,4 +4024,12 @@
clear();
}
}
+
+ private PushOneCommit.Result createWorkInProgressChange() throws Exception {
+ return pushTo("refs/for/master%wip");
+ }
+
+ private BranchApi createBranch(String branch) throws Exception {
+ return createBranch(new Branch.NameKey(project, branch));
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
new file mode 100644
index 0000000..f087b78
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeSubmitRequirementIT.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitRequirement;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.SubmitRequirementInfo;
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.project.SubmitRuleOptions;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.rules.SubmitRule;
+import com.google.inject.Module;
+import java.util.ArrayList;
+import java.util.Collection;
+import org.junit.Test;
+
+public class ChangeSubmitRequirementIT extends AbstractDaemonTest {
+ private static final SubmitRequirement req =
+ SubmitRequirement.builder()
+ .setType("custom_rule")
+ .setFallbackText("Fallback text")
+ .addCustomValue("key", "value")
+ .build();
+ private static final SubmitRequirementInfo reqInfo =
+ new SubmitRequirementInfo(
+ "NOT_READY", "Fallback text", "custom_rule", ImmutableMap.of("key", "value"));
+
+ @Override
+ public Module createModule() {
+ return new FactoryModule() {
+ @Override
+ public void configure() {
+ bind(SubmitRule.class)
+ .annotatedWith(Exports.named("CustomSubmitRule"))
+ .to(CustomSubmitRule.class);
+ }
+ };
+ }
+
+ @Test
+ public void checkSubmitRequirementIsPropagated() throws Exception {
+ PushOneCommit.Result r = createChange();
+
+ ChangeInfo result = gApi.changes().id(r.getChangeId()).get();
+ assertThat(result.requirements).containsExactly(reqInfo);
+ }
+
+ private static class CustomSubmitRule implements SubmitRule {
+ @Override
+ public Collection<SubmitRecord> evaluate(ChangeData changeData, SubmitRuleOptions options) {
+ SubmitRecord record = new SubmitRecord();
+ record.labels = new ArrayList<>();
+ record.status = SubmitRecord.Status.NOT_READY;
+ record.requirements = ImmutableList.of(req);
+ return ImmutableList.of(record);
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
index 4c97ac1..b23b2bf 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -45,7 +45,6 @@
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import java.util.EnumSet;
import java.util.List;
@@ -59,34 +58,46 @@
@NoHttpd
public class StickyApprovalsIT extends AbstractDaemonTest {
+
@Before
public void setup() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ // Overwrite "Code-Review" label that is inherited from All-Projects.
+ // This way changes to the "Code Review" label don't affect other tests.
+ LabelType codeReview =
+ category(
+ "Code-Review",
+ value(2, "Looks good to me, approved"),
+ value(1, "Looks good to me, but someone else must approve"),
+ value(0, "No score"),
+ value(-1, "I would prefer that you didn't submit this"),
+ value(-2, "Do not submit"));
+ codeReview.setCopyAllScoresIfNoChange(false);
+ u.getConfig().getLabelSections().put(codeReview.getName(), codeReview);
- // Overwrite "Code-Review" label that is inherited from All-Projects.
- // This way changes to the "Code Review" label don't affect other tests.
- LabelType codeReview =
- category(
- "Code-Review",
- value(2, "Looks good to me, approved"),
- value(1, "Looks good to me, but someone else must approve"),
- value(0, "No score"),
- value(-1, "I would prefer that you didn't submit this"),
- value(-2, "Do not submit"));
- codeReview.setCopyAllScoresIfNoChange(false);
- cfg.getLabelSections().put(codeReview.getName(), codeReview);
+ LabelType verified =
+ category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+ verified.setCopyAllScoresIfNoChange(false);
+ u.getConfig().getLabelSections().put(verified.getName(), verified);
- LabelType verified =
- category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
- verified.setCopyAllScoresIfNoChange(false);
- cfg.getLabelSections().put(verified.getName(), verified);
-
- AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
- String heads = RefNames.REFS_HEADS + "*";
- Util.allow(
- cfg, Permission.forLabel(Util.codeReview().getName()), -2, 2, registeredUsers, heads);
- Util.allow(cfg, Permission.forLabel(Util.verified().getName()), -1, 1, registeredUsers, heads);
- saveProjectConfig(project, cfg);
+ AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+ String heads = RefNames.REFS_HEADS + "*";
+ Util.allow(
+ u.getConfig(),
+ Permission.forLabel(Util.codeReview().getName()),
+ -2,
+ 2,
+ registeredUsers,
+ heads);
+ Util.allow(
+ u.getConfig(),
+ Permission.forLabel(Util.verified().getName()),
+ -1,
+ 1,
+ registeredUsers,
+ heads);
+ u.save();
+ }
}
@Test
@@ -97,9 +108,10 @@
@Test
public void stickyOnMinScore() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().get("Code-Review").setCopyMinScore(true);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().get("Code-Review").setCopyMinScore(true);
+ u.save();
+ }
for (ChangeKind changeKind :
EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
@@ -118,9 +130,10 @@
@Test
public void stickyOnMaxScore() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().get("Code-Review").setCopyMaxScore(true);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().get("Code-Review").setCopyMaxScore(true);
+ u.save();
+ }
for (ChangeKind changeKind :
EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
@@ -139,9 +152,10 @@
@Test
public void stickyOnTrivialRebase() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().get("Code-Review").setCopyAllScoresOnTrivialRebase(true);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().get("Code-Review").setCopyAllScoresOnTrivialRebase(true);
+ u.save();
+ }
String changeId = createChange(TRIVIAL_REBASE);
vote(admin, changeId, 2, 1);
@@ -184,9 +198,10 @@
@Test
public void stickyOnNoCodeChange() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().get("Verified").setCopyAllScoresIfNoCodeChange(true);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().get("Verified").setCopyAllScoresIfNoCodeChange(true);
+ u.save();
+ }
String changeId = createChange(NO_CODE_CHANGE);
vote(admin, changeId, 2, 1);
@@ -207,9 +222,13 @@
@Test
public void stickyOnMergeFirstParentUpdate() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().get("Code-Review").setCopyAllScoresOnMergeFirstParentUpdate(true);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .getLabelSections()
+ .get("Code-Review")
+ .setCopyAllScoresOnMergeFirstParentUpdate(true);
+ u.save();
+ }
String changeId = createChange(MERGE_FIRST_PARENT_UPDATE);
vote(admin, changeId, 2, 1);
@@ -230,10 +249,11 @@
@Test
public void removedVotesNotSticky() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().get("Code-Review").setCopyAllScoresOnTrivialRebase(true);
- cfg.getLabelSections().get("Verified").setCopyAllScoresIfNoCodeChange(true);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().get("Code-Review").setCopyAllScoresOnTrivialRebase(true);
+ u.getConfig().getLabelSections().get("Verified").setCopyAllScoresIfNoCodeChange(true);
+ u.save();
+ }
for (ChangeKind changeKind :
EnumSet.of(REWORK, TRIVIAL_REBASE, NO_CODE_CHANGE, MERGE_FIRST_PARENT_UPDATE, NO_CHANGE)) {
@@ -259,10 +279,11 @@
@Test
public void stickyAcrossMultiplePatchSets() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().get("Code-Review").setCopyMaxScore(true);
- cfg.getLabelSections().get("Verified").setCopyAllScoresIfNoCodeChange(true);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().get("Code-Review").setCopyMaxScore(true);
+ u.getConfig().getLabelSections().get("Verified").setCopyAllScoresIfNoCodeChange(true);
+ u.save();
+ }
String changeId = createChange(REWORK);
vote(admin, changeId, 2, 1);
@@ -280,10 +301,11 @@
@Test
public void copyMinMaxAcrossMultiplePatchSets() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().get("Code-Review").setCopyMaxScore(true);
- cfg.getLabelSections().get("Code-Review").setCopyMinScore(true);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().get("Code-Review").setCopyMaxScore(true);
+ u.getConfig().getLabelSections().get("Code-Review").setCopyMinScore(true);
+ u.save();
+ }
// Vote max score on PS1
String changeId = createChange(REWORK);
@@ -320,9 +342,10 @@
@Test
public void deleteStickyVote() throws Exception {
String label = "Code-Review";
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().get(label).setCopyMaxScore(true);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().get(label).setCopyMaxScore(true);
+ u.save();
+ }
// Vote max score on PS1
String changeId = createChange(REWORK);
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index c444fbd..4e3f048 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -28,14 +28,17 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.truth.Correspondence;
import com.google.common.util.concurrent.AtomicLongMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.ProjectResetter;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.GlobalCapability;
@@ -91,6 +94,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
@@ -121,6 +125,7 @@
@Inject private PeriodicGroupIndexer slaveGroupIndexer;
@Inject private DynamicSet<GroupIndexedListener> groupIndexedListeners;
@Inject private Sequences seq;
+ @Inject private AccountOperations accountOperations;
@Before
public void setTimeForTesting() {
@@ -177,23 +182,25 @@
@Test
public void cachedGroupsForMemberAreUpdatedOnMemberAdditionAndRemoval() throws Exception {
- TestAccount account = createUniqueAccount("user", "User");
+ String username = name("user");
+ com.google.gerrit.acceptance.testsuite.account.TestAccount account =
+ accountOperations.newAccount().username(username).create();
// Fill the cache for the observed account.
- groupIncludeCache.getGroupsWithMember(account.getId());
+ groupIncludeCache.getGroupsWithMember(account.accountId());
String groupName = createGroup("users");
AccountGroup.UUID groupUuid = new AccountGroup.UUID(gApi.groups().id(groupName).get().id);
- gApi.groups().id(groupName).addMembers(account.username);
+ gApi.groups().id(groupName).addMembers(username);
Collection<AccountGroup.UUID> groupsWithMemberAfterAddition =
- groupIncludeCache.getGroupsWithMember(account.getId());
+ groupIncludeCache.getGroupsWithMember(account.accountId());
assertThat(groupsWithMemberAfterAddition).contains(groupUuid);
- gApi.groups().id(groupName).removeMembers(account.username);
+ gApi.groups().id(groupName).removeMembers(username);
Collection<AccountGroup.UUID> groupsWithMemberAfterRemoval =
- groupIncludeCache.getGroupsWithMember(account.getId());
+ groupIncludeCache.getGroupsWithMember(account.accountId());
assertThat(groupsWithMemberAfterRemoval).doesNotContain(groupUuid);
}
@@ -214,20 +221,54 @@
@Test
public void addMultipleMembers() throws Exception {
String g = createGroup("users");
- TestAccount u1 = createUniqueAccount("u1", "Full Name 1");
- TestAccount u2 = createUniqueAccount("u2", "Full Name 2");
- gApi.groups().id(g).addMembers(u1.username, u2.username);
- assertMembers(g, u1, u2);
+
+ String u1 = name("u1");
+ accountOperations.newAccount().username(u1).create();
+ String u2 = name("u2");
+ accountOperations.newAccount().username(u2).create();
+
+ gApi.groups().id(g).addMembers(u1, u2);
+
+ List<AccountInfo> members = gApi.groups().id(g).members();
+ assertThat(members)
+ .comparingElementsUsing(getAccountToUsernameCorrespondence())
+ .containsExactly(u1, u2);
}
@Test
- public void addMembersWithAtSign() throws Exception {
+ public void membersWithAtSignInUsernameCanBeAdded() throws Exception {
String g = createGroup("users");
- TestAccount u1 = createUniqueAccount("u1", "Full Name 1");
- TestAccount u2_at = createUniqueAccount("u2@something", "Full Name 2 With At");
- TestAccount u2 = createUniqueAccount("u2", "Full Name 2 Without At");
- gApi.groups().id(g).addMembers(u1.username, u2_at.username, u2.username);
- assertMembers(g, u1, u2_at, u2);
+ String usernameWithAt = name("u1@something");
+ accountOperations.newAccount().username(usernameWithAt).create();
+
+ gApi.groups().id(g).addMembers(usernameWithAt);
+
+ List<AccountInfo> members = gApi.groups().id(g).members();
+ assertThat(members)
+ .comparingElementsUsing(getAccountToUsernameCorrespondence())
+ .containsExactly(usernameWithAt);
+ }
+
+ @Test
+ public void membersWithAtSignInUsernameAreNotConfusedWithSimilarUsernames() throws Exception {
+ String g = createGroup("users");
+ String usernameWithAt = name("u1@something");
+ accountOperations.newAccount().username(usernameWithAt).create();
+ String usernameWithoutAt = name("u1something");
+ accountOperations.newAccount().username(usernameWithoutAt).create();
+ String usernameOnlyPrefix = name("u1");
+ accountOperations.newAccount().username(usernameOnlyPrefix).create();
+ String usernameOnlySuffix = name("something");
+ accountOperations.newAccount().username(usernameOnlySuffix).create();
+
+ gApi.groups()
+ .id(g)
+ .addMembers(usernameWithAt, usernameWithoutAt, usernameOnlyPrefix, usernameOnlySuffix);
+
+ List<AccountInfo> members = gApi.groups().id(g).members();
+ assertThat(members)
+ .comparingElementsUsing(getAccountToUsernameCorrespondence())
+ .containsExactly(usernameWithAt, usernameWithoutAt, usernameOnlyPrefix, usernameOnlySuffix);
}
@Test
@@ -370,17 +411,19 @@
@Test
public void cachedGroupsForMemberAreUpdatedOnGroupCreation() throws Exception {
- TestAccount account = createUniqueAccount("user", "User");
+ com.google.gerrit.acceptance.testsuite.account.TestAccount account =
+ accountOperations.newAccount().create();
// Fill the cache for the observed account.
- groupIncludeCache.getGroupsWithMember(account.id);
+ groupIncludeCache.getGroupsWithMember(account.accountId());
GroupInput groupInput = new GroupInput();
groupInput.name = name("Users");
- groupInput.members = ImmutableList.of(account.username);
+ groupInput.members = ImmutableList.of(String.valueOf(account.accountId().get()));
GroupInfo group = gApi.groups().create(groupInput).get();
- Collection<AccountGroup.UUID> groups = groupIncludeCache.getGroupsWithMember(account.id);
+ Collection<AccountGroup.UUID> groups =
+ groupIncludeCache.getGroupsWithMember(account.accountId());
assertThat(groups).containsExactly(new AccountGroup.UUID(group.id));
}
@@ -622,28 +665,41 @@
@Test
public void listNonEmptyGroupMembers() throws Exception {
String group = createGroup("group");
- String user1 = createAccount("user1", group);
- String user2 = createAccount("user2", group);
+ String user1 = name("user1");
+ accountOperations.newAccount().username(user1).create();
+ String user2 = name("user2");
+ accountOperations.newAccount().username(user2).create();
+ gApi.groups().id(group).addMembers(user1, user2);
+
assertMembers(gApi.groups().id(group).members(), user1, user2);
}
@Test
public void listOneGroupMember() throws Exception {
String group = createGroup("group");
- String user = createAccount("user1", group);
+ String user = name("user1");
+ accountOperations.newAccount().username(user).create();
+ gApi.groups().id(group).addMembers(user);
+
assertMembers(gApi.groups().id(group).members(), user);
}
@Test
public void listGroupMembersRecursively() throws Exception {
String gx = createGroup("gx");
- String ux = createAccount("ux", gx);
+ String ux = name("ux");
+ accountOperations.newAccount().username(ux).create();
+ gApi.groups().id(gx).addMembers(ux);
String gy = createGroup("gy");
- String uy = createAccount("uy", gy);
+ String uy = name("uy");
+ accountOperations.newAccount().username(uy).create();
+ gApi.groups().id(gy).addMembers(uy);
String gz = createGroup("gz");
- String uz = createAccount("uz", gz);
+ String uz = name("uz");
+ accountOperations.newAccount().username(uz).create();
+ gApi.groups().id(gz).addMembers(uz);
gApi.groups().id(gx).addGroups(gy);
gApi.groups().id(gy).addGroups(gz);
@@ -916,7 +972,7 @@
// Verify "sub-group" has been deleted.
try {
gApi.groups().id(uuid.get()).get();
- fail();
+ fail("expected ResourceNotFoundException");
} catch (ResourceNotFoundException e) {
}
}
@@ -1050,8 +1106,9 @@
@Test
public void pushCustomInheritanceForAllUsersFails() throws Exception {
- TestRepository<InMemoryRepository> repo = cloneProject(allUsers, RefNames.REFS_CONFIG);
-
+ TestRepository<InMemoryRepository> repo = cloneProject(allUsers);
+ GitUtil.fetch(repo, RefNames.REFS_CONFIG + ":" + RefNames.REFS_CONFIG);
+ repo.reset(RefNames.REFS_CONFIG);
String config =
gApi.projects()
.name(allUsers.get())
@@ -1308,6 +1365,21 @@
}
}
+ private static Correspondence<AccountInfo, String> getAccountToUsernameCorrespondence() {
+ return new Correspondence<AccountInfo, String>() {
+ @Override
+ public boolean compare(AccountInfo actualAccount, String expectedName) {
+ String username = actualAccount == null ? null : actualAccount.username;
+ return Objects.equals(username, expectedName);
+ }
+
+ @Override
+ public String toString() {
+ return "has username";
+ }
+ };
+ }
+
private void assertStaleGroupAndReindex(AccountGroup.UUID groupUuid) throws IOException {
// Evict group from cache to be sure that we use the index state for staleness checks.
groupCache.evict(groupUuid);
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index 4791d4c..2b1416a 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -18,6 +18,7 @@
import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.config.AccessCheckInfo;
@@ -182,6 +183,19 @@
}
@Test
+ public void httpGet() throws Exception {
+ RestResponse rep =
+ adminRestSession.get(
+ "/projects/"
+ + normalProject.get()
+ + "/check.access"
+ + "?ref=refs/heads/master&perm=viewPrivateChanges&account="
+ + user.email);
+ rep.assertOK();
+ assertThat(rep.getEntityContent()).contains("403");
+ }
+
+ @Test
public void accessible() throws Exception {
List<TestCase> inputs =
ImmutableList.of(
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index 84cfb0d..b4a05fc 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -324,7 +324,7 @@
ConfigInput input = createTestConfigInput();
setApiUser(user);
exception.expect(AuthException.class);
- exception.expectMessage("write config not permitted");
+ exception.expectMessage("write refs/meta/config not permitted");
gApi.projects().name(project.get()).config(input);
}
@@ -359,7 +359,7 @@
gApi.projects().name(project.get()).branch("test").create(new BranchInput());
setApiUser(user);
exception.expect(AuthException.class);
- exception.expectMessage("set head not permitted");
+ exception.expectMessage("set HEAD not permitted for refs/heads/test");
gApi.projects().name(project.get()).head("test");
}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 6a9b542..4c8f53f 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -23,6 +23,7 @@
import static java.util.stream.Collectors.toMap;
import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GitUtil;
@@ -82,6 +83,11 @@
@Before
public void setUp() throws Exception {
+ // Reduce flakiness of tests. (If tests aren't fast enough, we would use a fall-back
+ // computation, which might yield different results.)
+ baseConfig.setString("cache", "diff", "timeout", "1 minute");
+ baseConfig.setString("cache", "diff_intraline", "timeout", "1 minute");
+
intraline = baseConfig.getBoolean(TEST_PARAMETER_MARKER, "intraline", false);
ObjectId headCommit = testRepo.getRepository().resolve("HEAD");
@@ -1254,6 +1260,66 @@
}
@Test
+ public void intralineEditsInNonRebaseHunksAreIdentified() throws Exception {
+ assume().that(intraline).isTrue();
+
+ Function<String, String> contentModification =
+ fileContent -> fileContent.replace("Line 1\n", "Line one\n");
+ addModifiedPatchSet(changeId, FILE_NAME, contentModification);
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+ assertThat(diffInfo).content().element(0).linesOfA().containsExactly("Line 1");
+ assertThat(diffInfo).content().element(0).linesOfB().containsExactly("Line one");
+ assertThat(diffInfo)
+ .content()
+ .element(0)
+ .intralineEditsOfA()
+ .containsExactly(ImmutableList.of(5, 1));
+ assertThat(diffInfo)
+ .content()
+ .element(0)
+ .intralineEditsOfB()
+ .containsExactly(ImmutableList.of(5, 3));
+ assertThat(diffInfo).content().element(0).isNotDueToRebase();
+ assertThat(diffInfo).content().element(1).commonLines().hasSize(99);
+ }
+
+ @Test
+ public void intralineEditsInRebaseHunksAreIdentified() throws Exception {
+ assume().that(intraline).isTrue();
+
+ String newFileContent = FILE_CONTENT.replace("Line 1\n", "Line one\n");
+ ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
+
+ rebaseChangeOn(changeId, commit2);
+ Function<String, String> contentModification =
+ fileContent -> fileContent.replace("Line 50\n", "Line fifty\n");
+ addModifiedPatchSet(changeId, FILE_NAME, contentModification);
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(initialPatchSetId).get();
+ assertThat(diffInfo).content().element(0).linesOfA().containsExactly("Line 1");
+ assertThat(diffInfo).content().element(0).linesOfB().containsExactly("Line one");
+ assertThat(diffInfo)
+ .content()
+ .element(0)
+ .intralineEditsOfA()
+ .containsExactly(ImmutableList.of(5, 1));
+ assertThat(diffInfo)
+ .content()
+ .element(0)
+ .intralineEditsOfB()
+ .containsExactly(ImmutableList.of(5, 3));
+ assertThat(diffInfo).content().element(0).isDueToRebase();
+ assertThat(diffInfo).content().element(1).commonLines().hasSize(48);
+ assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 50");
+ assertThat(diffInfo).content().element(2).linesOfB().containsExactly("Line fifty");
+ assertThat(diffInfo).content().element(2).isNotDueToRebase();
+ assertThat(diffInfo).content().element(3).commonLines().hasSize(50);
+ }
+
+ @Test
public void closeNonRebaseHunksAreCombinedForIntralineOptimizations() throws Exception {
assume().that(intraline).isTrue();
@@ -1365,6 +1431,47 @@
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
}
+ @Test
+ public void closeNonRebaseHunksNextToRebaseHunksAreCombinedForIntralineOptimizations()
+ throws Exception {
+ assume().that(intraline).isTrue();
+
+ String fileContent = FILE_CONTENT.replace("Line 5\n", "{\n").replace("Line 7\n", "{\n");
+ ObjectId commit2 = addCommit(commit1, FILE_NAME, fileContent);
+ rebaseChangeOn(changeId, commit2);
+ String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
+
+ String newFileContent = fileContent.replace("Line 8\n", "Line eight!\n");
+ ObjectId commit3 = addCommit(commit1, FILE_NAME, newFileContent);
+ rebaseChangeOn(changeId, commit3);
+
+ addModifiedPatchSet(
+ changeId,
+ FILE_NAME,
+ content -> content.replace("Line 4\n", "Line four\n").replace("Line 6\n", "Line six\n"));
+
+ DiffInfo diffInfo =
+ getDiffRequest(changeId, CURRENT, FILE_NAME).withBase(previousPatchSetId).get();
+ assertThat(diffInfo).content().element(0).commonLines().hasSize(3);
+ assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 4", "{", "Line 6");
+ assertThat(diffInfo)
+ .content()
+ .element(1)
+ .linesOfB()
+ .containsExactly("Line four", "{", "Line six");
+ assertThat(diffInfo).content().element(1).isNotDueToRebase();
+ assertThat(diffInfo).content().element(2).commonLines().hasSize(1);
+ assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 8");
+ assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line eight!");
+ assertThat(diffInfo).content().element(3).isDueToRebase();
+ assertThat(diffInfo).content().element(4).commonLines().hasSize(92);
+
+ Map<String, FileInfo> changedFiles =
+ gApi.changes().id(changeId).current().files(previousPatchSetId);
+ assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2);
+ assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
+ }
+
private void assertDiffForNewFile(
PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception {
DiffInfo diff =
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 1871343..3514e8e 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -518,7 +518,7 @@
in.message = r1.getCommit().getFullMessage();
try {
gApi.changes().id(t1).current().cherryPick(in);
- fail();
+ fail("expected ResourceConflictException");
} catch (ResourceConflictException e) {
assertThat(e.getMessage())
.isEqualTo(
diff --git a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index f4bb2a9..91a1278 100644
--- a/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/javatests/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -56,7 +56,6 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import com.google.gerrit.server.restapi.change.ChangeEdits.EditMessage;
import com.google.gerrit.server.restapi.change.ChangeEdits.Post;
@@ -601,11 +600,12 @@
@Test
public void editCommitMessageCopiesLabelScores() throws Exception {
String cr = "Code-Review";
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- LabelType codeReview = Util.codeReview();
- codeReview.setCopyAllScoresIfNoCodeChange(true);
- cfg.getLabelSections().put(cr, codeReview);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType codeReview = Util.codeReview();
+ codeReview.setCopyAllScoresIfNoCodeChange(true);
+ u.getConfig().getLabelSections().put(cr, codeReview);
+ u.save();
+ }
ReviewInput r = new ReviewInput();
r.labels = ImmutableMap.of(cr, (short) 1);
@@ -846,16 +846,18 @@
private <T> T readContentFromJson(RestResponse r, Class<T> clazz) throws Exception {
r.assertOK();
- JsonReader jsonReader = new JsonReader(r.getReader());
- jsonReader.setLenient(true);
- return newGson().fromJson(jsonReader, clazz);
+ try (JsonReader jsonReader = new JsonReader(r.getReader())) {
+ jsonReader.setLenient(true);
+ return newGson().fromJson(jsonReader, clazz);
+ }
}
private <T> T readContentFromJson(RestResponse r, TypeToken<T> typeToken) throws Exception {
r.assertOK();
- JsonReader jsonReader = new JsonReader(r.getReader());
- jsonReader.setLenient(true);
- return newGson().fromJson(jsonReader, typeToken.getType());
+ try (JsonReader jsonReader = new JsonReader(r.getReader())) {
+ jsonReader.setLenient(true);
+ return newGson().fromJson(jsonReader, typeToken.getType());
+ }
}
private String readContentFromJson(RestResponse r) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 2ae4133..df146c7 100644
--- a/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/javatests/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -85,7 +85,6 @@
import com.google.gerrit.server.git.validators.CommitValidators.ChangeIdValidator;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testing.FakeEmailSender.Message;
@@ -138,19 +137,25 @@
}
@Before
- public void setUp() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- patchSetLock = Util.patchSetLock();
- cfg.getLabelSections().put(patchSetLock.getName(), patchSetLock);
- AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
- Util.allow(
- cfg, Permission.forLabel(patchSetLock.getName()), 0, 1, anonymousUsers, "refs/heads/*");
- saveProjectConfig(cfg);
+ public void setUpPatchSetLock() throws Exception {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ patchSetLock = Util.patchSetLock();
+ u.getConfig().getLabelSections().put(patchSetLock.getName(), patchSetLock);
+ AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
+ Util.allow(
+ u.getConfig(),
+ Permission.forLabel(patchSetLock.getName()),
+ 0,
+ 1,
+ anonymousUsers,
+ "refs/heads/*");
+ u.save();
+ }
grant(project, "refs/heads/*", Permission.LABEL + "Patch-Set-Lock");
}
@After
- public void tearDown() throws Exception {
+ public void resetPublishCommentOnPushOption() throws Exception {
setApiUser(admin);
GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences();
prefs.publishCommentsOnPush = false;
@@ -932,12 +937,13 @@
public void pushWithMultipleApprovals() throws Exception {
LabelType Q =
category("Custom-Label", value(1, "Positive"), value(0, "No score"), value(-1, "Negative"));
- ProjectConfig config = projectCache.checkedGet(project).getConfig();
AccountGroup.UUID anon = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
String heads = "refs/heads/*";
- Util.allow(config, Permission.forLabel("Custom-Label"), -1, 1, anon, heads);
- config.getLabelSections().put(Q.getName(), Q);
- saveProjectConfig(project, config);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.allow(u.getConfig(), Permission.forLabel("Custom-Label"), -1, 1, anon, heads);
+ u.getConfig().getLabelSections().put(Q.getName(), Q);
+ u.save();
+ }
RevCommit c =
commitBuilder()
@@ -1116,7 +1122,7 @@
r.assertOkStatus();
setUseSignedOffBy(InheritableBoolean.TRUE);
- blockForgeCommitter(project, "refs/heads/master");
+ block(project, "refs/heads/master", Permission.FORGE_COMMITTER, REGISTERED_USERS);
push =
pushFactory.create(
@@ -1213,12 +1219,14 @@
@Test
public void pushSameCommitTwice() throws Exception {
- ProjectConfig config = projectCache.checkedGet(project).getConfig();
- config
- .getProject()
- .setBooleanConfig(
- BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET, InheritableBoolean.TRUE);
- saveProjectConfig(project, config);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .getProject()
+ .setBooleanConfig(
+ BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
+ InheritableBoolean.TRUE);
+ u.save();
+ }
PushOneCommit push =
pushFactory.create(
@@ -1240,12 +1248,14 @@
@Test
public void pushSameCommitTwiceWhenIndexFailed() throws Exception {
- ProjectConfig config = projectCache.checkedGet(project).getConfig();
- config
- .getProject()
- .setBooleanConfig(
- BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET, InheritableBoolean.TRUE);
- saveProjectConfig(project, config);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .getProject()
+ .setBooleanConfig(
+ BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
+ InheritableBoolean.TRUE);
+ u.save();
+ }
PushOneCommit push =
pushFactory.create(
@@ -1456,11 +1466,12 @@
+ "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
+ " commit");
- ProjectConfig config = projectCache.checkedGet(project).getConfig();
- config
- .getProject()
- .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
- saveProjectConfig(project, config);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .getProject()
+ .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
+ u.save();
+ }
pushForReviewRejected(
testRepo,
@@ -1481,11 +1492,12 @@
+ "Squash the commits with the same Change-Id or ensure Change-Ids are unique for each"
+ " commit");
- ProjectConfig config = projectCache.checkedGet(project).getConfig();
- config
- .getProject()
- .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
- saveProjectConfig(project, config);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .getProject()
+ .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
+ u.save();
+ }
pushForReviewRejected(
testRepo,
@@ -1646,11 +1658,12 @@
@Test
public void pushNewPatchsetOverridingStickyLabel() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- LabelType codeReview = Util.codeReview();
- codeReview.setCopyMaxScore(true);
- cfg.getLabelSections().put(codeReview.getName(), codeReview);
- saveProjectConfig(cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType codeReview = Util.codeReview();
+ codeReview.setCopyMaxScore(true);
+ u.getConfig().getLabelSections().put(codeReview.getName(), codeReview);
+ u.save();
+ }
PushOneCommit.Result r = pushTo("refs/for/master%l=Code-Review+2");
r.assertOkStatus();
@@ -2239,11 +2252,16 @@
private void grantSkipValidation(Project.NameKey project, String ref, AccountGroup.UUID groupUuid)
throws Exception {
// See SKIP_VALIDATION implementation in default permission backend.
- ProjectConfig config = projectCache.checkedGet(project).getConfig();
- Util.allow(config, Permission.FORGE_AUTHOR, groupUuid, ref);
- Util.allow(config, Permission.FORGE_COMMITTER, groupUuid, ref);
- Util.allow(config, Permission.FORGE_SERVER, groupUuid, ref);
- Util.allow(config, Permission.PUSH_MERGE, groupUuid, "refs/for/" + ref);
- saveProjectConfig(project, config);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.allow(u.getConfig(), Permission.FORGE_AUTHOR, groupUuid, ref);
+ Util.allow(u.getConfig(), Permission.FORGE_COMMITTER, groupUuid, ref);
+ Util.allow(u.getConfig(), Permission.FORGE_SERVER, groupUuid, ref);
+ Util.allow(u.getConfig(), Permission.PUSH_MERGE, groupUuid, "refs/for/" + ref);
+ u.save();
+ }
+ }
+
+ private PushOneCommit.Result amendChange(String changeId, String ref) throws Exception {
+ return amendChange(changeId, ref, admin, testRepo);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
index 752da69..87ac022 100644
--- a/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ForcePushIT.java
@@ -51,7 +51,7 @@
pushFactory.create(db, admin.getIdent(), testRepo, "change2", "b.txt", "content");
push2.setForce(true);
PushOneCommit.Result r2 = push2.to("refs/heads/master");
- r2.assertErrorStatus("non-fast forward");
+ r2.assertErrorStatus("need 'Force Push' privilege.");
}
@Test
@@ -94,7 +94,7 @@
@Test
public void deleteAllowedWithDeletePermission() throws Exception {
- grant(project, "refs/*", Permission.PUSH, true);
+ grant(project, "refs/*", Permission.DELETE, true);
assertDeleteRef(OK);
}
diff --git a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
index 72f2e0d..954ca8b 100644
--- a/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/ImplicitMergeCheckIT.java
@@ -21,7 +21,6 @@
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
-import com.google.gerrit.server.project.ProjectConfig;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
@@ -82,10 +81,12 @@
}
private void setRejectImplicitMerges() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getProject()
- .setBooleanConfig(BooleanProjectConfig.REJECT_IMPLICIT_MERGES, InheritableBoolean.TRUE);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .getProject()
+ .setBooleanConfig(BooleanProjectConfig.REJECT_IMPLICIT_MERGES, InheritableBoolean.TRUE);
+ u.save();
+ }
}
private PushOneCommit.Result push(String ref, String subject, String fileName, String content)
diff --git a/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
new file mode 100644
index 0000000..b362a36
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/git/PushPermissionsIT.java
@@ -0,0 +1,384 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.git.testing.PushResultSubject.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.testing.Util;
+import java.util.Arrays;
+import java.util.function.Consumer;
+import org.eclipse.jgit.api.PushCommand;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.TrackingRefUpdate;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PushPermissionsIT extends AbstractDaemonTest {
+ @Before
+ public void setUp() throws Exception {
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ ProjectConfig cfg = u.getConfig();
+ cfg.getProject()
+ .setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, InheritableBoolean.FALSE);
+
+ // Remove push-related permissions, so they can be added back individually by test methods.
+ removeAllBranchPermissions(
+ cfg,
+ Permission.ADD_PATCH_SET,
+ Permission.CREATE,
+ Permission.DELETE,
+ Permission.PUSH,
+ Permission.PUSH_MERGE,
+ Permission.SUBMIT);
+ removeAllGlobalCapabilities(cfg, GlobalCapability.ADMINISTRATE_SERVER);
+
+ // Include some auxiliary permissions.
+ Util.allow(cfg, Permission.FORGE_AUTHOR, REGISTERED_USERS, "refs/*");
+ Util.allow(cfg, Permission.FORGE_COMMITTER, REGISTERED_USERS, "refs/*");
+
+ u.save();
+ }
+ }
+
+ @Test
+ public void fastForwardUpdateDenied() throws Exception {
+ testRepo.branch("HEAD").commit().create();
+ PushResult r = push("HEAD:refs/heads/master");
+ assertThat(r)
+ .onlyRef("refs/heads/master")
+ .isRejected("prohibited by Gerrit: ref update access denied");
+ assertThat(r)
+ .hasMessages(
+ "Branch refs/heads/master:",
+ "You are not allowed to perform this operation.",
+ "To push into this reference you need 'Push' rights.",
+ "User: admin",
+ "Please read the documentation and contact an administrator",
+ "if you feel the configuration is incorrect");
+ assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
+ }
+
+ @Test
+ public void nonFastForwardUpdateDenied() throws Exception {
+ ObjectId commit = testRepo.commit().create();
+ PushResult r = push("+" + commit.name() + ":refs/heads/master");
+ assertThat(r).onlyRef("refs/heads/master").isRejected("need 'Force Push' privilege.");
+ assertThat(r).hasNoMessages();
+ // TODO(dborowitz): Why does this not mention refs?
+ assertThat(r).hasProcessed(ImmutableMap.of());
+ }
+
+ @Test
+ public void deleteDenied() throws Exception {
+ PushResult r = push(":refs/heads/master");
+ assertThat(r).onlyRef("refs/heads/master").isRejected("cannot delete references");
+ assertThat(r)
+ .hasMessages(
+ "Branch refs/heads/master:",
+ "You need 'Delete Reference' rights or 'Push' rights with the ",
+ "'Force Push' flag set to delete references.",
+ "User: admin",
+ "Please read the documentation and contact an administrator",
+ "if you feel the configuration is incorrect");
+ assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
+ }
+
+ @Test
+ public void createDenied() throws Exception {
+ testRepo.branch("HEAD").commit().create();
+ PushResult r = push("HEAD:refs/heads/newbranch");
+ assertThat(r)
+ .onlyRef("refs/heads/newbranch")
+ .isRejected("prohibited by Gerrit: create not permitted for refs/heads/newbranch");
+ assertThat(r).hasNoMessages();
+ assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
+ }
+
+ @Test
+ public void groupRefsByMessage() throws Exception {
+ try (Repository repo = repoManager.openRepository(project)) {
+ TestRepository<?> tr = new TestRepository<>(repo);
+ tr.branch("foo").commit().create();
+ tr.branch("bar").commit().create();
+ }
+
+ testRepo.branch("HEAD").commit().create();
+ PushResult r = push(":refs/heads/foo", ":refs/heads/bar", "HEAD:refs/heads/master");
+ assertThat(r).ref("refs/heads/foo").isRejected("cannot delete references");
+ assertThat(r).ref("refs/heads/bar").isRejected("cannot delete references");
+ assertThat(r)
+ .ref("refs/heads/master")
+ .isRejected("prohibited by Gerrit: ref update access denied");
+ assertThat(r)
+ .hasMessages(
+ "Branches refs/heads/foo, refs/heads/bar:",
+ "You need 'Delete Reference' rights or 'Push' rights with the ",
+ "'Force Push' flag set to delete references.",
+ "Branch refs/heads/master:",
+ "You are not allowed to perform this operation.",
+ "To push into this reference you need 'Push' rights.",
+ "User: admin",
+ "Please read the documentation and contact an administrator",
+ "if you feel the configuration is incorrect");
+ }
+
+ @Test
+ public void readOnlyProjectRejectedBeforeTestingPermissions() throws Exception {
+ try (Repository repo = repoManager.openRepository(project)) {
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getProject().setState(ProjectState.READ_ONLY);
+ u.save();
+ }
+ }
+
+ PushResult r = push(":refs/heads/master");
+ assertThat(r)
+ .onlyRef("refs/heads/master")
+ .isRejected("prohibited by Gerrit: project state does not permit write");
+ assertThat(r).hasNoMessages();
+ assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
+ }
+
+ @Test
+ public void refsMetaConfigUpdateRequiresProjectOwner() throws Exception {
+ grant(project, "refs/meta/config", Permission.PUSH, false, REGISTERED_USERS);
+
+ forceFetch("refs/meta/config");
+ ObjectId commit = testRepo.branch("refs/meta/config").commit().create();
+ PushResult r = push(commit.name() + ":refs/meta/config");
+ assertThat(r)
+ .onlyRef("refs/meta/config")
+ // ReceiveCommits theoretically has a different message when a WRITE_CONFIG check fails, but
+ // it never gets there, since DefaultPermissionBackend special-cases refs/meta/config and
+ // denies UPDATE if the user is not a project owner.
+ .isRejected("prohibited by Gerrit: ref update access denied");
+ assertThat(r)
+ .hasMessages(
+ "Branch refs/meta/config:",
+ "You are not allowed to perform this operation.",
+ "Configuration changes can only be pushed by project owners",
+ "who also have 'Push' rights on refs/meta/config",
+ "User: admin",
+ "Please read the documentation and contact an administrator",
+ "if you feel the configuration is incorrect");
+ assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
+
+ grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
+
+ // Re-fetch refs/meta/config from the server because the grant changed it, and we want a
+ // fast-forward.
+ forceFetch("refs/meta/config");
+ commit = testRepo.branch("refs/meta/config").commit().create();
+
+ assertThat(push(commit.name() + ":refs/meta/config")).onlyRef("refs/meta/config").isOk();
+ }
+
+ @Test
+ public void createChangeDenied() throws Exception {
+ testRepo.branch("HEAD").commit().create();
+ PushResult r = push("HEAD:refs/for/master");
+ assertThat(r)
+ .onlyRef("refs/for/master")
+ .isRejected("create change not permitted for refs/heads/master");
+ assertThat(r)
+ .hasMessages(
+ "Branch refs/heads/master:",
+ "You need 'Push' rights to upload code review requests.",
+ "Verify that you are pushing to the right branch.",
+ "User: admin",
+ "Please read the documentation and contact an administrator",
+ "if you feel the configuration is incorrect");
+ assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
+ }
+
+ @Test
+ public void updateBySubmitDenied() throws Exception {
+ grant(project, "refs/for/refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
+
+ ObjectId commit = testRepo.branch("HEAD").commit().create();
+ assertThat(push("HEAD:refs/for/master")).onlyRef("refs/for/master").isOk();
+ gApi.changes().id(commit.name()).current().review(ReviewInput.approve());
+
+ PushResult r = push("HEAD:refs/for/master%submit");
+ assertThat(r)
+ .onlyRef("refs/for/master%submit")
+ .isRejected("update by submit not permitted for refs/heads/master");
+ assertThat(r).hasNoMessages();
+ assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
+ }
+
+ @Test
+ public void addPatchSetDenied() throws Exception {
+ grant(project, "refs/for/refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
+ setApiUser(user);
+ ChangeInput ci = new ChangeInput();
+ ci.project = project.get();
+ ci.branch = "master";
+ ci.subject = "A change";
+ Change.Id id = new Change.Id(gApi.changes().create(ci).get()._number);
+
+ setApiUser(admin);
+ ObjectId ps1Id = forceFetch(new PatchSet.Id(id, 1).toRefName());
+ ObjectId ps2Id = testRepo.amend(ps1Id).add("file", "content").create();
+ PushResult r = push(ps2Id.name() + ":refs/for/master");
+ assertThat(r)
+ .onlyRef("refs/for/master")
+ .isRejected("cannot add patch set to " + id.get() + ".");
+ assertThat(r).hasNoMessages();
+ assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
+ }
+
+ @Test
+ public void skipValidationDenied() throws Exception {
+ grant(project, "refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
+
+ testRepo.branch("HEAD").commit().create();
+ PushResult r =
+ push(c -> c.setPushOptions(ImmutableList.of("skip-validation")), "HEAD:refs/heads/master");
+ assertThat(r)
+ .onlyRef("refs/heads/master")
+ .isRejected("skip validation not permitted for refs/heads/master");
+ assertThat(r).hasNoMessages();
+ assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
+ }
+
+ @Test
+ public void accessDatabaseForNoteDbDenied() throws Exception {
+ grant(project, "refs/heads/*", Permission.PUSH, false, REGISTERED_USERS);
+
+ testRepo.branch("HEAD").commit().create();
+ PushResult r =
+ push(
+ c -> c.setPushOptions(ImmutableList.of("notedb=allow")),
+ "HEAD:refs/changes/34/1234/meta");
+ // Same rejection message regardless of whether NoteDb is actually enabled.
+ assertThat(r)
+ .onlyRef("refs/changes/34/1234/meta")
+ .isRejected("NoteDb update requires access database permission");
+ assertThat(r).hasNoMessages();
+ assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
+ }
+
+ @Test
+ public void administrateServerForUpdateParentDenied() throws Exception {
+ grant(project, "refs/meta/config", Permission.PUSH, false, REGISTERED_USERS);
+ grant(project, "refs/*", Permission.OWNER, false, REGISTERED_USERS);
+
+ String project2 = name("project2");
+ gApi.projects().create(project2);
+
+ ObjectId oldId = forceFetch("refs/meta/config");
+
+ Config cfg = new BlobBasedConfig(null, testRepo.getRepository(), oldId, "project.config");
+ cfg.setString("access", null, "inheritFrom", project2);
+ ObjectId newId =
+ testRepo.branch("refs/meta/config").commit().add("project.config", cfg.toText()).create();
+
+ PushResult r = push(newId.name() + ":refs/meta/config");
+ assertThat(r)
+ .onlyRef("refs/meta/config")
+ .isRejected("invalid project configuration: only Gerrit admin can set parent");
+ assertThat(r).hasNoMessages();
+ assertThat(r).hasProcessed(ImmutableMap.of("refs", 1));
+ }
+
+ private static void removeAllBranchPermissions(ProjectConfig cfg, String... permissions) {
+ cfg.getAccessSections()
+ .stream()
+ .filter(
+ s ->
+ s.getName().startsWith("refs/heads/")
+ || s.getName().startsWith("refs/for/")
+ || s.getName().equals("refs/*"))
+ .forEach(s -> Arrays.stream(permissions).forEach(s::removePermission));
+ }
+
+ private static void removeAllGlobalCapabilities(ProjectConfig cfg, String... capabilities) {
+ Arrays.stream(capabilities)
+ .forEach(
+ c ->
+ cfg.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true)
+ .getPermission(c, true)
+ .getRules()
+ .clear());
+ }
+
+ private PushResult push(String... refSpecs) throws Exception {
+ return push(c -> {}, refSpecs);
+ }
+
+ private PushResult push(Consumer<PushCommand> setUp, String... refSpecs) throws Exception {
+ PushCommand cmd =
+ testRepo
+ .git()
+ .push()
+ .setRemote("origin")
+ .setRefSpecs(Arrays.stream(refSpecs).map(RefSpec::new).collect(toList()));
+ setUp.accept(cmd);
+ Iterable<PushResult> results = cmd.call();
+ assertWithMessage("expected 1 PushResult").that(results).hasSize(1);
+ return results.iterator().next();
+ }
+
+ private ObjectId forceFetch(String ref) throws Exception {
+ TrackingRefUpdate u =
+ testRepo.git().fetch().setRefSpecs("+" + ref + ":" + ref).call().getTrackingRefUpdate(ref);
+ assertThat(u).isNotNull();
+ switch (u.getResult()) {
+ case NEW:
+ case FAST_FORWARD:
+ case FORCED:
+ break;
+ case IO_FAILURE:
+ case LOCK_FAILURE:
+ case NOT_ATTEMPTED:
+ case NO_CHANGE:
+ case REJECTED:
+ case REJECTED_CURRENT_BRANCH:
+ case REJECTED_MISSING_OBJECT:
+ case REJECTED_OTHER_REASON:
+ case RENAMED:
+ default:
+ assert_().fail("fetch failed to update local %s: %s", ref, u.getResult());
+ break;
+ }
+ return u.getNewObjectId();
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index c64cdfb..45294fb 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -52,7 +52,6 @@
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testing.NoteDbMode;
@@ -106,20 +105,22 @@
private void setUpPermissions() throws Exception {
// Remove read permissions for all users besides admin. This method is idempotent, so is safe
// to call on every test setup.
- ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig();
- for (AccessSection sec : pc.getAccessSections()) {
- sec.removePermission(Permission.READ);
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ for (AccessSection sec : u.getConfig().getAccessSections()) {
+ sec.removePermission(Permission.READ);
+ }
+ Util.allow(u.getConfig(), Permission.READ, admins, "refs/*");
+ u.save();
}
- Util.allow(pc, Permission.READ, admins, "refs/*");
- saveProjectConfig(allProjects, pc);
// Remove all read permissions on All-Users. This method is idempotent, so is safe to call on
// every test setup.
- pc = projectCache.checkedGet(allUsers).getConfig();
- for (AccessSection sec : pc.getAccessSections()) {
- sec.removePermission(Permission.READ);
+ try (ProjectConfigUpdate u = updateProject(allUsers)) {
+ for (AccessSection sec : u.getConfig().getAccessSections()) {
+ sec.removePermission(Permission.READ);
+ }
+ u.save();
}
- saveProjectConfig(allUsers, pc);
}
private static String changeRefPrefix(Change.Id id) {
@@ -171,11 +172,12 @@
@Test
public void uploadPackAllRefsVisibleNoRefsMetaConfig() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.allow(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
- Util.allow(cfg, Permission.READ, admins, RefNames.REFS_CONFIG);
- Util.doNotInherit(cfg, Permission.READ, RefNames.REFS_CONFIG);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.allow(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
+ Util.allow(u.getConfig(), Permission.READ, admins, RefNames.REFS_CONFIG);
+ Util.doNotInherit(u.getConfig(), Permission.READ, RefNames.REFS_CONFIG);
+ u.save();
+ }
setApiUser(user);
assertUploadPackRefs(
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java b/javatests/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java
index fac594a..5404fdd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/CapabilityInfo.java
@@ -36,6 +36,7 @@
public boolean viewConnections;
public boolean viewPlugins;
public boolean viewQueue;
+ public boolean viewAccess;
static class QueryLimit {
short min;
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
index a1dc8a2..f3fe68a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/EmailIT.java
@@ -15,19 +15,54 @@
package com.google.gerrit.acceptance.rest.account;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
import static java.util.stream.Collectors.toSet;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.accounts.EmailApi;
import com.google.gerrit.extensions.api.accounts.EmailInput;
import com.google.gerrit.extensions.common.EmailInfo;
import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.AccountsUpdate;
+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.account.Realm;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.DisableReverseDnsLookup;
import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import org.junit.Test;
public class EmailIT extends AbstractDaemonTest {
+ @Inject private @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider;
+ @Inject private ExternalIds externalIds;
+ @Inject private SchemaFactory<ReviewDb> reviewDbProvider;
+ @Inject private AuthConfig authConfig;
+ @Inject private @AnonymousCowardName String anonymousCowardName;
+ @Inject private @CanonicalWebUrl Provider<String> canonicalUrl;
+ @Inject private @DisableReverseDnsLookup Boolean disableReverseDnsLookup;
+ @Inject private EmailExpander emailExpander;
+ @Inject private Provider<Emails> emails;
@Test
public void addEmail() throws Exception {
@@ -82,6 +117,143 @@
assertThat(getEmails()).doesNotContain(email);
}
+ @Test
+ public void setPreferredEmailToEmailOfMailToExternalId() throws Exception {
+ String email = "foo@example.com";
+ createEmail(email);
+ assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
+
+ resetCurrentApiUser();
+ gApi.accounts().self().email(email).setPreferred();
+ assertThat(gApi.accounts().self().get().email).isEqualTo(email);
+ }
+
+ @Test
+ public void setPreferredEmailToEmailOfExternalExternalId() throws Exception {
+ String email = "foo@example.com";
+ accountsUpdateProvider
+ .get()
+ .update(
+ "Add External ID",
+ admin.id,
+ u ->
+ u.addExternalId(
+ ExternalId.createWithEmail(
+ ExternalId.SCHEME_EXTERNAL, "foo", admin.id, email)));
+ assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
+
+ resetCurrentApiUser();
+ gApi.accounts().self().email(email).setPreferred();
+ assertThat(gApi.accounts().self().get().email).isEqualTo(email);
+ }
+
+ @Test
+ public void setPreferredEmailToNonExistingEmail() throws Exception {
+ String email = "non-existing@example.com";
+ exception.expect(ResourceNotFoundException.class);
+ exception.expectMessage("Not found: " + email);
+ gApi.accounts().self().email(email).setPreferred();
+ }
+
+ @Test
+ public void setPreferredEmailToEmailOfOtherAccount() throws Exception {
+ exception.expect(ResourceNotFoundException.class);
+ exception.expectMessage("Not found: " + user.email);
+ gApi.accounts().self().email(user.email).setPreferred();
+ }
+
+ @Test
+ public void setPreferredEmailWithOtherCase() throws Exception {
+ String email = "foo@example.com";
+ createEmail(email);
+ assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
+
+ resetCurrentApiUser();
+ String emailOtherCase = email.toUpperCase();
+ gApi.accounts().self().email(emailOtherCase).setPreferred();
+ assertThat(gApi.accounts().self().get().email).isEqualTo(email);
+ }
+
+ @Test
+ public void setPreferredEmailToEmailFromCustomRealmThatDoesntExistAsExternalId()
+ throws Exception {
+ String email = "foo@example.com";
+ ExternalId.Key mailtoExtIdKey = ExternalId.Key.create(ExternalId.SCHEME_MAILTO, email);
+ assertThat(externalIds.get(mailtoExtIdKey)).isEmpty();
+ assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
+
+ Context oldCtx = createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id, email));
+ try {
+ gApi.accounts().self().email(email).setPreferred();
+ Optional<ExternalId> mailtoExtId = externalIds.get(mailtoExtIdKey);
+ assertThat(mailtoExtId).isPresent();
+ assertThat(mailtoExtId.get().accountId()).isEqualTo(admin.id);
+ assertThat(gApi.accounts().self().get().email).isEqualTo(email);
+ } finally {
+ atrScope.set(oldCtx);
+ }
+ }
+
+ @Test
+ public void setPreferredEmailToEmailFromCustomRealmThatBelongsToOtherAccount() throws Exception {
+ ExternalId mailToExtId = ExternalId.createEmail(user.id, user.email);
+ assertThat(externalIds.get(mailToExtId.key())).isPresent();
+
+ Context oldCtx =
+ createContextWithCustomRealm(new RealmWithAdditionalEmails(admin.id, user.email));
+ try {
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage("email in use by another account");
+ gApi.accounts().self().email(user.email).setPreferred();
+ } finally {
+ atrScope.set(oldCtx);
+ }
+ }
+
+ @Test
+ public void emailApi() throws Exception {
+ String email = "foo@example.com";
+ assertThat(getEmails()).doesNotContain(email);
+
+ // Create email
+ EmailInput emailInput = new EmailInput();
+ emailInput.email = email;
+ emailInput.noConfirmation = true;
+ gApi.accounts().self().createEmail(emailInput);
+ assertThat(getEmails()).contains(email);
+ assertThat(gApi.accounts().self().get().email).isNotEqualTo(email);
+
+ // Get email
+ resetCurrentApiUser();
+ EmailApi emailApi = gApi.accounts().self().email(email);
+ EmailInfo emailInfo = emailApi.get();
+ assertThat(emailInfo.email).isEqualTo(email);
+ assertThat(emailInfo.preferred).isNull();
+ assertThat(emailInfo.pendingConfirmation).isNull();
+
+ // Set as preferred email
+ emailApi.setPreferred();
+ assertThat(gApi.accounts().self().get().email).isEqualTo(email);
+
+ // Get email again (now it's the preferred email)
+ resetCurrentApiUser();
+ emailApi = gApi.accounts().self().email(email);
+ emailInfo = emailApi.get();
+ assertThat(emailInfo.email).isEqualTo(email);
+ assertThat(emailInfo.preferred).isTrue();
+ assertThat(emailInfo.pendingConfirmation).isNull();
+
+ // Delete email
+ emailApi.delete();
+ assertThat(getEmails()).doesNotContain(email);
+
+ // Now the email is no longer found
+ resetCurrentApiUser();
+ emailApi = gApi.accounts().self().email(email);
+ exception.expect(ResourceNotFoundException.class);
+ emailApi.get();
+ }
+
private Set<String> getEmails() throws Exception {
RestResponse r = adminRestSession.get("/accounts/self/emails");
r.assertOK();
@@ -96,4 +268,38 @@
RestResponse r = adminRestSession.put("/accounts/self/emails/" + email, input);
r.assertCreated();
}
+
+ private Context createContextWithCustomRealm(Realm realm) {
+ IdentifiedUser.GenericFactory userFactory =
+ new IdentifiedUser.GenericFactory(
+ authConfig,
+ realm,
+ anonymousCowardName,
+ canonicalUrl,
+ disableReverseDnsLookup,
+ accountCache,
+ groupBackend);
+ return atrScope.set(atrScope.newContext(reviewDbProvider, null, userFactory.create(admin.id)));
+ }
+
+ private class RealmWithAdditionalEmails extends DefaultRealm {
+ private final Multimap<Account.Id, String> additionalEmails;
+
+ public RealmWithAdditionalEmails(Account.Id accountId, String email) {
+ this(ImmutableMultimap.of(accountId, email));
+ }
+
+ public RealmWithAdditionalEmails(Multimap<Account.Id, String> additionalEmails) {
+ super(emailExpander, emails, authConfig);
+ this.additionalEmails = additionalEmails;
+ }
+
+ @Override
+ public boolean hasEmailAddress(IdentifiedUser user, String email) {
+ if (additionalEmails.containsEntry(user.getAccountId(), email)) {
+ return true;
+ }
+ return super.hasEmailAddress(user, email);
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index f6e1e56..ab4ea40 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -64,6 +64,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -419,8 +420,8 @@
assertThat(extIds).containsExactlyElementsIn(parseableExtIds);
for (ExternalId parseableExtId : parseableExtIds) {
- ExternalId extId = externalIds.get(parseableExtId.key());
- assertThat(extId).isEqualTo(parseableExtId);
+ Optional<ExternalId> extId = externalIds.get(parseableExtId.key());
+ assertThat(extId).hasValue(parseableExtId);
}
}
@@ -719,8 +720,8 @@
"Create Account with Bad External ID",
accountId,
u -> u.addExternalId(ExternalId.create(extIdKey, accountId)));
- ExternalId extId = externalIds.get(extIdKey);
- assertThat(extId.accountId()).isEqualTo(accountId);
+ Optional<ExternalId> extId = externalIds.get(extIdKey);
+ assertThat(extId.map(ExternalId::accountId)).hasValue(accountId);
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdNotesIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdNotesIT.java
index 211ae18..60535c0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdNotesIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ExternalIdNotesIT.java
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance.rest.account;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
import static org.hamcrest.CoreMatchers.instanceOf;
import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -25,6 +26,7 @@
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.inject.Inject;
import java.io.IOException;
+import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Repository;
import org.junit.Test;
@@ -271,9 +273,9 @@
private void assertExternalId(ExternalId.Key extIdKey, @Nullable String expectedEmail)
throws Exception {
- ExternalId extId = externalIds.get(extIdKey);
- assertThat(extId).named(extIdKey.get()).isNotNull();
- assertThat(extId.email()).named("email of " + extIdKey.get()).isEqualTo(expectedEmail);
+ Optional<ExternalId> extId = externalIds.get(extIdKey);
+ assertThat(extId).named(extIdKey.get()).isPresent();
+ assertThat(extId.get().email()).named("email of " + extIdKey.get()).isEqualTo(expectedEmail);
}
private void expectException(String message) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index 8cfb8fa..e6f61fa 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -60,7 +60,6 @@
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.account.AccountControl;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
@@ -139,26 +138,38 @@
}
@Test
+ @GerritConfig(name = "change.strictLabels", value = "true")
public void voteOnBehalfOfInvalidLabel() throws Exception {
allowCodeReviewOnBehalfOf();
- PushOneCommit.Result r = createChange();
- RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
- ReviewInput in = new ReviewInput();
+ String changeId = createChange().getChangeId();
+ ReviewInput in = new ReviewInput().label("Not-A-Label", 5);
in.onBehalfOf = user.id.toString();
- in.label("Not-A-Label", 5);
exception.expect(BadRequestException.class);
exception.expectMessage("label \"Not-A-Label\" is not a configured label");
- revision.review(in);
+ gApi.changes().id(changeId).current().review(in);
+ }
+
+ @Test
+ public void voteOnBehalfOfInvalidLabelIgnoredWithoutStrictLabels() throws Exception {
+ allowCodeReviewOnBehalfOf();
+
+ String changeId = createChange().getChangeId();
+ ReviewInput in = new ReviewInput().label("Code-Review", 1).label("Not-A-Label", 5);
+ in.onBehalfOf = user.id.toString();
+ gApi.changes().id(changeId).current().review(in);
+
+ assertThat(gApi.changes().id(changeId).get().labels).doesNotContainKey("Not-A-Label");
}
@Test
public void voteOnBehalfOfLabelNotPermitted() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- LabelType verified = Util.verified();
- cfg.getLabelSections().put(verified.getName(), verified);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType verified = Util.verified();
+ u.getConfig().getLabelSections().put(verified.getName(), verified);
+ u.save();
+ }
PushOneCommit.Result r = createChange();
RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
@@ -362,7 +373,7 @@
SubmitInput in = new SubmitInput();
in.onBehalfOf = admin2.email;
exception.expect(AuthException.class);
- exception.expectMessage("submit as not permitted");
+ exception.expectMessage("submit on behalf of other users not permitted");
gApi.changes().id(project.get() + "~master~" + r.getChangeId()).current().submit(in);
}
@@ -539,44 +550,54 @@
}
private void allowCodeReviewOnBehalfOf() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- LabelType codeReviewType = Util.codeReview();
- String forCodeReviewAs = Permission.forLabelAs(codeReviewType.getName());
- String heads = "refs/heads/*";
- AccountGroup.UUID uuid = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
- Util.allow(cfg, forCodeReviewAs, -1, 1, uuid, heads);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType codeReviewType = Util.codeReview();
+ String forCodeReviewAs = Permission.forLabelAs(codeReviewType.getName());
+ String heads = "refs/heads/*";
+ AccountGroup.UUID uuid = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+ Util.allow(u.getConfig(), forCodeReviewAs, -1, 1, uuid, heads);
+ u.save();
+ }
}
private void allowSubmitOnBehalfOf() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- String heads = "refs/heads/*";
- AccountGroup.UUID uuid = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
- Util.allow(cfg, Permission.SUBMIT_AS, uuid, heads);
- Util.allow(cfg, Permission.SUBMIT, uuid, heads);
- LabelType codeReviewType = Util.codeReview();
- Util.allow(cfg, Permission.forLabel(codeReviewType.getName()), -2, 2, uuid, heads);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ String heads = "refs/heads/*";
+ AccountGroup.UUID uuid = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+ Util.allow(u.getConfig(), Permission.SUBMIT_AS, uuid, heads);
+ Util.allow(u.getConfig(), Permission.SUBMIT, uuid, heads);
+ LabelType codeReviewType = Util.codeReview();
+ Util.allow(u.getConfig(), Permission.forLabel(codeReviewType.getName()), -2, 2, uuid, heads);
+ u.save();
+ }
}
private void blockRead(GroupInfo group) throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.block(cfg, Permission.READ, new AccountGroup.UUID(group.id), "refs/heads/master");
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.block(
+ u.getConfig(), Permission.READ, new AccountGroup.UUID(group.id), "refs/heads/master");
+ u.save();
+ }
}
private void allowRunAs() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
- Util.allow(
- cfg, GlobalCapability.RUN_AS, systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID());
- saveProjectConfig(allProjects, cfg);
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ Util.allow(
+ u.getConfig(),
+ GlobalCapability.RUN_AS,
+ systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID());
+ u.save();
+ }
}
private void removeRunAs() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
- Util.remove(
- cfg, GlobalCapability.RUN_AS, systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID());
- saveProjectConfig(allProjects, cfg);
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ Util.remove(
+ u.getConfig(),
+ GlobalCapability.RUN_AS,
+ systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID());
+ u.save();
+ }
}
private static Header runAsHeader(Object user) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index c9e04f7..507b74b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -36,6 +36,7 @@
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.changes.ChangeApi;
@@ -71,7 +72,6 @@
import com.google.gerrit.server.change.TestSubmitInput;
import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import com.google.gerrit.server.restapi.change.Submit;
import com.google.gerrit.server.update.BatchUpdate;
@@ -203,7 +203,8 @@
// change 2 is not approved, but we ignore labels
approve(change3.getChangeId());
- try (BinaryResult request = submitPreview(change4.getChangeId())) {
+ try (BinaryResult request =
+ gApi.changes().id(change4.getChangeId()).current().submitPreview()) {
assertThat(getSubmitType()).isEqualTo(SubmitType.CHERRY_PICK);
submit(change4.getChangeId());
} catch (RestApiException e) {
@@ -327,11 +328,13 @@
public void noSelfSubmit() throws Exception {
// create project where submit is blocked for the change owner
Project.NameKey p = createProject("p");
- ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
- Util.block(cfg, Permission.SUBMIT, CHANGE_OWNER, "refs/*");
- Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*");
- Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*");
- saveProjectConfig(p, cfg);
+ try (ProjectConfigUpdate u = updateProject(p)) {
+ Util.block(u.getConfig(), Permission.SUBMIT, CHANGE_OWNER, "refs/*");
+ Util.allow(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*");
+ Util.allow(
+ u.getConfig(), Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*");
+ u.save();
+ }
TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
@@ -351,11 +354,13 @@
public void onlySelfSubmit() throws Exception {
// create project where only the change owner can submit
Project.NameKey p = createProject("p");
- ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
- Util.block(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/*");
- Util.allow(cfg, Permission.SUBMIT, CHANGE_OWNER, "refs/*");
- Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*");
- saveProjectConfig(p, cfg);
+ try (ProjectConfigUpdate u = updateProject(p)) {
+ Util.block(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/*");
+ Util.allow(u.getConfig(), Permission.SUBMIT, CHANGE_OWNER, "refs/*");
+ Util.allow(
+ u.getConfig(), Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*");
+ u.save();
+ }
TestRepository<InMemoryRepository> repo = cloneProject(p, admin);
PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo);
@@ -525,7 +530,7 @@
@Test
public void submitWorkInProgressChange() throws Exception {
- PushOneCommit.Result change = createWorkInProgressChange();
+ PushOneCommit.Result change = pushTo("refs/for/master%wip");
Change.Id num = change.getChange().getId();
submitWithConflict(
change.getChangeId(),
@@ -567,12 +572,14 @@
// |
// C0 -- Master
//
- ProjectConfig config = projectCache.checkedGet(project).getConfig();
- config
- .getProject()
- .setBooleanConfig(
- BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET, InheritableBoolean.TRUE);
- saveProjectConfig(project, config);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig()
+ .getProject()
+ .setBooleanConfig(
+ BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
+ InheritableBoolean.TRUE);
+ u.save();
+ }
PushOneCommit push1 =
pushFactory.create(
@@ -912,7 +919,7 @@
testRepo.git().fetch().call();
RevWalk rw = testRepo.getRevWalk();
RevCommit master = rw.parseCommit(getRemoteHead(project, "master"));
- RevCommit patchSet = parseCurrentRevision(rw, change);
+ RevCommit patchSet = parseCurrentRevision(rw, change.getChangeId());
assertThat(rw.isMergedInto(patchSet, master)).isTrue();
assertThat(input.generateLockFailures).containsExactly(false);
@@ -954,13 +961,13 @@
repoA.git().fetch().call();
RevWalk rwA = repoA.getRevWalk();
RevCommit masterA = rwA.parseCommit(getRemoteHead(name("project-a"), "master"));
- RevCommit change1Ps = parseCurrentRevision(rwA, change1);
+ RevCommit change1Ps = parseCurrentRevision(rwA, change1.getChangeId());
assertThat(rwA.isMergedInto(change1Ps, masterA)).isTrue();
repoB.git().fetch().call();
RevWalk rwB = repoB.getRevWalk();
RevCommit masterB = rwB.parseCommit(getRemoteHead(name("project-b"), "master"));
- RevCommit change2Ps = parseCurrentRevision(rwB, change2);
+ RevCommit change2Ps = parseCurrentRevision(rwB, change2.getChangeId());
assertThat(rwB.isMergedInto(change2Ps, masterB)).isTrue();
assertThat(input.generateLockFailures).containsExactly(false);
@@ -1304,4 +1311,19 @@
return out.toString();
}
}
+
+ private TestRepository<?> createProjectWithPush(
+ String name, @Nullable Project.NameKey parent, SubmitType submitType) throws Exception {
+ Project.NameKey project = createProject(name, parent, true, submitType);
+ grant(project, "refs/heads/*", Permission.PUSH);
+ grant(project, "refs/for/refs/heads/*", Permission.SUBMIT);
+ return cloneProject(project);
+ }
+
+ protected PushOneCommit.Result createChange(
+ String subject, String fileName, String content, String topic) throws Exception {
+ PushOneCommit push =
+ pushFactory.create(db, admin.getIdent(), testRepo, subject, fileName, content);
+ return push.to("refs/for/master/" + name(topic));
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
index d86650b..0a92cfb 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -36,7 +36,6 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.change.TestSubmitInput;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
@@ -58,17 +57,18 @@
@Test
@TestProjectInput(useContentMerge = InheritableBoolean.TRUE)
public void submitWithRebaseWithoutAddPatchSetPermission() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.block(cfg, Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/*");
- Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*");
- Util.allow(
- cfg,
- Permission.forLabel(Util.codeReview().getName()),
- -2,
- 2,
- REGISTERED_USERS,
- "refs/heads/*");
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.block(u.getConfig(), Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/*");
+ Util.allow(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*");
+ Util.allow(
+ u.getConfig(),
+ Permission.forLabel(Util.codeReview().getName()),
+ -2,
+ 2,
+ REGISTERED_USERS,
+ "refs/heads/*");
+ u.save();
+ }
submitWithRebase(user);
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index 066af79..f89f2a1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -22,6 +22,7 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.extensions.api.changes.ActionVisitor;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -71,6 +72,14 @@
}
}
+ protected Map<String, ActionInfo> getActions(String id) throws Exception {
+ return gApi.changes().id(id).revision(1).actions();
+ }
+
+ protected String getETag(String id) throws Exception {
+ return gApi.changes().id(id).current().etag();
+ }
+
@Test
public void revisionActionsOneChangePerTopicUnapproved() throws Exception {
String changeId = createChangeWithTopic().getChangeId();
@@ -452,4 +461,8 @@
assertThat(actions).containsKey("description");
assertThat(actions).containsKey("rebase");
}
+
+ private PushOneCommit.Result createChangeWithTopic() throws Exception {
+ return createChangeWithTopic(testRepo, "topic", "message", "a.txt", "content\n");
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
index fbd55bb..59b6e29 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeIncludedInIT.java
@@ -15,10 +15,12 @@
package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat;
+import static org.eclipse.jgit.lib.Constants.R_TAGS;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit.Result;
+import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.TagInput;
import com.google.gerrit.reviewdb.client.Branch;
@@ -47,7 +49,7 @@
.containsExactly("master");
assertThat(gApi.changes().id(result.getChangeId()).includedIn().tags).isEmpty();
- grantTagPermissions();
+ grant(project, R_TAGS + "*", Permission.CREATE_TAG);
gApi.projects().name(project.get()).tag("test-tag").create(new TagInput());
assertThat(gApi.changes().id(result.getChangeId()).includedIn().tags)
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index f1bba8a..ee5d3b0 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -830,9 +830,10 @@
private static <T> T readContentFromJson(RestResponse r, int expectedStatus, Class<T> clazz)
throws Exception {
r.assertStatus(expectedStatus);
- JsonReader jsonReader = new JsonReader(r.getReader());
- jsonReader.setLenient(true);
- return newGson().fromJson(jsonReader, clazz);
+ try (JsonReader jsonReader = new JsonReader(r.getReader())) {
+ jsonReader.setLenient(true);
+ return newGson().fromJson(jsonReader, clazz);
+ }
}
private static void assertReviewers(
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index 30aeb69..baf56de 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -30,7 +30,6 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
@@ -46,11 +45,12 @@
public class ConfigChangeIT extends AbstractDaemonTest {
@Before
public void setUp() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.allow(cfg, Permission.OWNER, REGISTERED_USERS, "refs/*");
- Util.allow(cfg, Permission.PUSH, REGISTERED_USERS, "refs/for/refs/meta/config");
- Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, RefNames.REFS_CONFIG);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.allow(u.getConfig(), Permission.OWNER, REGISTERED_USERS, "refs/*");
+ Util.allow(u.getConfig(), Permission.PUSH, REGISTERED_USERS, "refs/for/refs/meta/config");
+ Util.allow(u.getConfig(), Permission.SUBMIT, REGISTERED_USERS, RefNames.REFS_CONFIG);
+ u.save();
+ }
setApiUser(user);
fetchRefsMetaConfig();
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index ce949b4..6555fe8 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -24,7 +24,6 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import java.util.List;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -54,14 +53,15 @@
// Create a project and restrict its visibility to the group
Project.NameKey p = createProject("p");
- ProjectConfig cfg = projectCache.checkedGet(p).getConfig();
- Util.allow(
- cfg,
- Permission.READ,
- groupCache.get(new AccountGroup.NameKey(group)).get().getGroupUUID(),
- "refs/*");
- Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
- saveProjectConfig(p, cfg);
+ try (ProjectConfigUpdate u = updateProject(p)) {
+ Util.allow(
+ u.getConfig(),
+ Permission.READ,
+ groupCache.get(new AccountGroup.NameKey(group)).get().getGroupUUID(),
+ "refs/*");
+ Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
+ u.save();
+ }
// Clone it and push a change as a regular user
TestRepository<InMemoryRepository> repo = cloneProject(p, user);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
index 85093655..93ad2fe 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -34,7 +34,6 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.PersonIdent;
@@ -214,13 +213,19 @@
Branch.NameKey newBranch = new Branch.NameKey(r.getChange().change().getProject(), "moveTest");
createBranch(newBranch);
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- LabelType patchSetLock = Util.patchSetLock();
- cfg.getLabelSections().put(patchSetLock.getName(), patchSetLock);
- AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
- Util.allow(
- cfg, Permission.forLabel(patchSetLock.getName()), 0, 1, registeredUsers, "refs/heads/*");
- saveProjectConfig(cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType patchSetLock = Util.patchSetLock();
+ u.getConfig().getLabelSections().put(patchSetLock.getName(), patchSetLock);
+ AccountGroup.UUID registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+ Util.allow(
+ u.getConfig(),
+ Permission.forLabel(patchSetLock.getName()),
+ 0,
+ 1,
+ registeredUsers,
+ "refs/heads/*");
+ u.save();
+ }
grant(project, "refs/heads/*", Permission.LABEL + "Patch-Set-Lock");
revision(r).review(new ReviewInput().label("Patch-Set-Lock", 1));
@@ -244,11 +249,15 @@
configLabel(testLabelC, LabelFunction.NO_BLOCK);
AccountGroup.UUID registered = SystemGroupBackend.REGISTERED_USERS;
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.allow(cfg, Permission.forLabel(testLabelA), -1, +1, registered, "refs/heads/*");
- Util.allow(cfg, Permission.forLabel(testLabelB), -1, +1, registered, "refs/heads/*");
- Util.allow(cfg, Permission.forLabel(testLabelC), -1, +1, registered, "refs/heads/*");
- saveProjectConfig(cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.allow(
+ u.getConfig(), Permission.forLabel(testLabelA), -1, +1, registered, "refs/heads/*");
+ Util.allow(
+ u.getConfig(), Permission.forLabel(testLabelB), -1, +1, registered, "refs/heads/*");
+ Util.allow(
+ u.getConfig(), Permission.forLabel(testLabelC), -1, +1, registered, "refs/heads/*");
+ u.save();
+ }
String changeId = createChange().getChangeId();
gApi.changes().id(changeId).current().review(ReviewInput.reject());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index 2ebf3ca..ea8b98a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -122,7 +122,9 @@
PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content");
approve(change2.getChangeId());
- Map<String, ActionInfo> actions = getActions(change2.getChangeId());
+
+ Map<String, ActionInfo> actions =
+ gApi.changes().id(change2.getChangeId()).revision(1).actions();
assertThat(actions).containsKey("submit");
ActionInfo info = actions.get("submit");
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 19ca430..93b3e14 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -265,7 +265,7 @@
+ "and upload the rebased commit for review.";
// Get a preview before submitting:
- try (BinaryResult r = submitPreview(change1b.getChangeId())) {
+ try (BinaryResult r = gApi.changes().id(change1b.getChangeId()).current().submitPreview()) {
// We cannot just use the ExpectedException infrastructure as provided
// by AbstractDaemonTest, as then we'd stop early and not test the
// actual submit.
@@ -517,7 +517,8 @@
// get a preview before submitting:
File tempfile;
- try (BinaryResult request = submitPreview(change1.getChangeId(), "tgz")) {
+ try (BinaryResult request =
+ gApi.changes().id(change1.getChangeId()).current().submitPreview("tgz")) {
assertThat(request.getContentType()).isEqualTo("application/x-gzip");
tempfile = File.createTempFile("test", null);
request.writeTo(Files.newOutputStream(tempfile.toPath()));
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
index cea907d..95bc5a6 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
@@ -54,7 +54,7 @@
private TagType tagType;
@Before
- public void setup() throws Exception {
+ public void setUpTestEnvironment() throws Exception {
// clone with user to avoid inherited tag permissions of admin user
testRepo = cloneProject(project, user);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 87fd5cd..f7903dd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -156,6 +156,26 @@
}
@Test
+ public void createAccessChangeEmptyConfig() throws Exception {
+ try (Repository repo = repoManager.openRepository(newProjectName)) {
+ RefUpdate ru = repo.updateRef(RefNames.REFS_CONFIG);
+ ru.setForceUpdate(true);
+ assertThat(ru.delete()).isEqualTo(Result.FORCED);
+
+ ProjectAccessInput accessInput = newProjectAccessInput();
+ AccessSectionInfo accessSection = newAccessSectionInfo();
+ PermissionInfo read = newPermissionInfo();
+ PermissionRuleInfo pri = new PermissionRuleInfo(PermissionRuleInfo.Action.BLOCK, false);
+ read.rules.put(SystemGroupBackend.REGISTERED_USERS.get(), pri);
+ accessSection.permissions.put(Permission.READ, read);
+ accessInput.add.put(REFS_HEADS, accessSection);
+
+ ChangeInfo out = pApi().accessChange(accessInput);
+ assertThat(out.status).isEqualTo(ChangeStatus.NEW);
+ }
+ }
+
+ @Test
public void createAccessChange() throws Exception {
allow(newProjectName, RefNames.REFS_CONFIG, Permission.READ, REGISTERED_USERS);
// User can see the branch
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CommitIncludedInIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CommitIncludedInIT.java
index 61f14e4..c0a413b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/CommitIncludedInIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CommitIncludedInIT.java
@@ -15,10 +15,12 @@
package com.google.gerrit.acceptance.rest.project;
import static com.google.common.truth.Truth.assertThat;
+import static org.eclipse.jgit.lib.Constants.R_TAGS;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.changes.IncludedInInfo;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.TagInput;
@@ -46,7 +48,7 @@
assertThat(getIncludedIn(result.getCommit().getId()).branches).containsExactly("master");
assertThat(getIncludedIn(result.getCommit().getId()).tags).isEmpty();
- grantTagPermissions();
+ grant(project, R_TAGS + "*", Permission.CREATE_TAG);
gApi.projects().name(result.getChange().project().get()).tag("test-tag").create(new TagInput());
assertThat(getIncludedIn(result.getCommit().getId()).tags).containsExactly("test-tag");
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
index 6e1a184..0632221 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -23,7 +23,6 @@
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.server.project.ProjectConfig;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
@@ -116,9 +115,10 @@
}
private void unblockRead() throws Exception {
- ProjectConfig pc = projectCache.checkedGet(project).getConfig();
- pc.getAccessSection("refs/*").remove(new Permission(Permission.READ));
- saveProjectConfig(project, pc);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getAccessSection("refs/*").remove(new Permission(Permission.READ));
+ u.save();
+ }
}
private void assertNotFound(ObjectId id) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index a5b0347..cd88a56 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -32,7 +32,6 @@
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import java.util.List;
import java.util.Map;
@@ -55,9 +54,10 @@
setApiUser(user);
assertThatNameList(gApi.projects().list().get()).contains(project);
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*");
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.block(u.getConfig(), Permission.READ, REGISTERED_USERS, "refs/*");
+ u.save();
+ }
assertThatNameList(filter(gApi.projects().list().get())).doesNotContain(project);
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index c9ba851..714751d 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -370,4 +370,11 @@
// Expected
}
}
+
+ private void grantTagPermissions() throws Exception {
+ grant(project, R_TAGS + "*", Permission.CREATE);
+ grant(project, R_TAGS + "", Permission.DELETE);
+ grant(project, R_TAGS + "*", Permission.CREATE_TAG);
+ grant(project, R_TAGS + "*", Permission.CREATE_SIGNED_TAG);
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java b/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
index d60056a..14b3858 100644
--- a/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/event/CommentAddedEventIT.java
@@ -33,7 +33,6 @@
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import com.google.inject.Inject;
import org.junit.After;
@@ -56,11 +55,24 @@
@Before
public void setUp() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
- Util.allow(cfg, Permission.forLabel(label.getName()), -1, 1, anonymousUsers, "refs/heads/*");
- Util.allow(cfg, Permission.forLabel(pLabel.getName()), 0, 1, anonymousUsers, "refs/heads/*");
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
+ Util.allow(
+ u.getConfig(),
+ Permission.forLabel(label.getName()),
+ -1,
+ 1,
+ anonymousUsers,
+ "refs/heads/*");
+ Util.allow(
+ u.getConfig(),
+ Permission.forLabel(pLabel.getName()),
+ 0,
+ 1,
+ anonymousUsers,
+ "refs/heads/*");
+ u.save();
+ }
eventListenerRegistration =
source.add(
@@ -78,10 +90,11 @@
}
private void saveLabelConfig() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().put(label.getName(), label);
- cfg.getLabelSections().put(pLabel.getName(), pLabel);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().put(label.getName(), label);
+ u.getConfig().getLabelSections().put(pLabel.getName(), pLabel);
+ u.save();
+ }
}
/* Need to lookup info for the label under test since there can be multiple
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 112d982..7096581 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -866,7 +866,8 @@
}
@Test
- public void addReviewerOnWipChangeAndStartReview() throws Exception {
+ public void addReviewerOnWipChangeAndStartReviewInNoteDb() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
StagedChange sc = stageWipChange();
ReviewInput in = ReviewInput.noScore().reviewer(other.email).setWorkInProgress(false);
gApi.changes().id(sc.changeId).revision("current").review(in);
@@ -877,6 +878,35 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ // TODO(logan): Should CCs be included?
+ assertThat(sender)
+ .sent("newchange", sc)
+ .to(other)
+ .cc(sc.reviewer)
+ .cc(sc.reviewerByEmail, sc.ccerByEmail)
+ .noOneElse();
+ assertThat(sender).notSent();
+ }
+
+ @Test
+ public void addReviewerOnWipChangeAndStartReviewInReviewDb() throws Exception {
+ assume().that(notesMigration.readChanges()).isFalse();
+ StagedChange sc = stageWipChange();
+ ReviewInput in = ReviewInput.noScore().reviewer(other.email).setWorkInProgress(false);
+ gApi.changes().id(sc.changeId).revision("current").review(in);
+ assertThat(sender)
+ .sent("comment", sc)
+ .cc(sc.reviewer, sc.ccer, other)
+ .cc(sc.reviewerByEmail, sc.ccerByEmail)
+ .bcc(sc.starrer)
+ .bcc(ALL_COMMENTS)
+ .noOneElse();
+ assertThat(sender)
+ .sent("newchange", sc)
+ .to(other)
+ .cc(sc.reviewer, sc.ccer)
+ .cc(sc.reviewerByEmail, sc.ccerByEmail)
+ .noOneElse();
assertThat(sender).notSent();
}
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index 33d7b00..f5ff61f 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -75,7 +75,6 @@
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.restapi.change.PostReview;
@@ -1574,17 +1573,23 @@
}
private void allowRunAs() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
- Util.allow(
- cfg, GlobalCapability.RUN_AS, systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
- saveProjectConfig(allProjects, cfg);
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ Util.allow(
+ u.getConfig(),
+ GlobalCapability.RUN_AS,
+ systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
+ u.save();
+ }
}
private void removeRunAs() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
- Util.remove(
- cfg, GlobalCapability.RUN_AS, systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
- saveProjectConfig(allProjects, cfg);
+ try (ProjectConfigUpdate u = updateProject(allProjects)) {
+ Util.remove(
+ u.getConfig(),
+ GlobalCapability.RUN_AS,
+ systemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
+ u.save();
+ }
}
private Map<String, List<CommentInfo>> getPublishedComments(Change.Id id) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
index 8d3ce6d..0257240 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/CustomLabelIT.java
@@ -43,7 +43,6 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import com.google.inject.Inject;
import org.junit.After;
@@ -65,11 +64,19 @@
@Before
public void setUp() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
- Util.allow(cfg, Permission.forLabel(label.getName()), -1, 1, anonymousUsers, "refs/heads/*");
- Util.allow(cfg, Permission.forLabel(P.getName()), 0, 1, anonymousUsers, "refs/heads/*");
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ AccountGroup.UUID anonymousUsers = systemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID();
+ Util.allow(
+ u.getConfig(),
+ Permission.forLabel(label.getName()),
+ -1,
+ 1,
+ anonymousUsers,
+ "refs/heads/*");
+ Util.allow(
+ u.getConfig(), Permission.forLabel(P.getName()), 0, 1, anonymousUsers, "refs/heads/*");
+ u.save();
+ }
eventListenerRegistration =
source.add(
@@ -288,10 +295,11 @@
value(-1, "I would prefer this is not merged as is"),
value(-2, "This shall not be merged"));
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
AccountGroup.UUID registered = SystemGroupBackend.REGISTERED_USERS;
- Util.allow(cfg, Permission.forLabel(testLabel), -2, +2, registered, "refs/heads/*");
- saveProjectConfig(cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.allow(u.getConfig(), Permission.forLabel(testLabel), -2, +2, registered, "refs/heads/*");
+ u.save();
+ }
PushOneCommit.Result result = createChange();
String changeId = result.getChangeId();
@@ -309,9 +317,11 @@
assertThat(gApi.changes().id(changeId).get().submittable).isTrue();
// Update admin's permitted range for 'Test-Label' to be -1...+1.
- Util.remove(cfg, Permission.forLabel(testLabel), registered, "refs/heads/*");
- Util.allow(cfg, Permission.forLabel(testLabel), -1, +1, registered, "refs/heads/*");
- saveProjectConfig(cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ Util.remove(u.getConfig(), Permission.forLabel(testLabel), registered, "refs/heads/*");
+ Util.allow(u.getConfig(), Permission.forLabel(testLabel), -1, +1, registered, "refs/heads/*");
+ u.save();
+ }
// Verify admin doesn't have +2 permission any more.
assertPermitted(gApi.changes().id(changeId).get(), testLabel, -1, 0, 1);
@@ -336,10 +346,11 @@
}
private void saveLabelConfig() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.getLabelSections().put(label.getName(), label);
- cfg.getLabelSections().put(P.getName(), P);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().getLabelSections().put(label.getName(), label);
+ u.getConfig().getLabelSections().put(P.getName(), P);
+ u.save();
+ }
}
private ChangeInfo getWithLabels(PushOneCommit.Result r) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 0f81b5f..ca0cae4 100644
--- a/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -31,7 +31,6 @@
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
import com.google.gerrit.server.git.NotifyConfig;
import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.testing.FakeEmailSender.Message;
import java.util.EnumSet;
import java.util.List;
@@ -51,9 +50,10 @@
nc.setTypes(EnumSet.of(NotifyType.NEW_PATCHSETS));
nc.setFilter("message:sekret");
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.putNotifyConfig("watch", nc);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().putNotifyConfig("watch", nc);
+ u.save();
+ }
PushOneCommit.Result r =
pushFactory
@@ -91,9 +91,10 @@
nc.setHeader(NotifyConfig.Header.TO);
nc.setTypes(EnumSet.of(NotifyType.NEW_CHANGES, NotifyType.ALL_COMMENTS));
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.putNotifyConfig("team", nc);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().putNotifyConfig("team", nc);
+ u.save();
+ }
sender.clear();
PushOneCommit.Result r =
@@ -122,9 +123,10 @@
nc.setHeader(NotifyConfig.Header.TO);
nc.setTypes(EnumSet.of(NotifyType.NEW_PATCHSETS));
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.putNotifyConfig("team", nc);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().putNotifyConfig("team", nc);
+ u.save();
+ }
PushOneCommit.Result r =
pushFactory
@@ -152,9 +154,10 @@
nc.setHeader(NotifyConfig.Header.TO);
nc.setTypes(EnumSet.of(NotifyType.NEW_CHANGES, NotifyType.ALL_COMMENTS));
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.putNotifyConfig("team", nc);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().putNotifyConfig("team", nc);
+ u.save();
+ }
sender.clear();
PushOneCommit.Result r =
@@ -182,9 +185,10 @@
nc.setHeader(NotifyConfig.Header.TO);
nc.setTypes(EnumSet.of(NotifyType.NEW_PATCHSETS));
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- cfg.putNotifyConfig("team", nc);
- saveProjectConfig(project, cfg);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ u.getConfig().putNotifyConfig("team", nc);
+ u.save();
+ }
PushOneCommit.Result r =
pushFactory
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
index ea42f47..9a5a899 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -26,8 +26,6 @@
import com.google.gerrit.acceptance.UseSsh;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.sshd.Commands;
-import com.jcraft.jsch.JSchException;
-import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -54,6 +52,7 @@
"ls-projects",
"ls-user-refs",
"plugin",
+ "reload-config",
"show-caches",
"show-connections",
"show-queue",
@@ -95,7 +94,9 @@
}
}),
"index",
- ImmutableList.of("activate", "changes", "project", "start"),
+ ImmutableList.of("changes", "project"), // "activate" and "start" are not included
+ "logging",
+ ImmutableList.of("ls", "set"),
"plugin",
ImmutableList.of("add", "enable", "install", "ls", "reload", "remove", "rm"),
"test-submit",
@@ -120,8 +121,7 @@
testCommandExecution(SLAVE_COMMANDS);
}
- private void testCommandExecution(Map<String, List<String>> commands)
- throws JSchException, IOException {
+ private void testCommandExecution(Map<String, List<String>> commands) throws Exception {
for (String root : commands.keySet()) {
for (String command : commands.get(root)) {
// We can't assert that adminSshSession.hasError() is false, because using the --help
diff --git a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
index aa7a987..95c585d 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/UploadArchiveIT.java
@@ -121,7 +121,8 @@
// Make sure the next one is not on the error channel
packet = in.readString();
- // 1 = DATA. It would be nicer to parse the OutputStream with SideBandInputStream from JGit, but
+ // 1 = DATA. It would be nicer to parse the OutputStream with SideBandInputStream from JGit,
+ // but
// that is currently not public.
char channel = packet.charAt(0);
if (channel != 1) {
diff --git a/javatests/com/google/gerrit/common/BUILD b/javatests/com/google/gerrit/common/BUILD
index d906284..ff19646 100644
--- a/javatests/com/google/gerrit/common/BUILD
+++ b/javatests/com/google/gerrit/common/BUILD
@@ -30,5 +30,6 @@
"//lib:guava",
"//lib:truth",
"//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
],
)
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index 567595b..70d7089 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -41,7 +41,6 @@
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/project/testing:project-test-util",
"//java/com/google/gerrit/testing:gerrit-test-util",
- "//javatests/com/google/gerrit/server/query:index-config",
"//javatests/com/google/gerrit/server/query/%s:abstract_query_tests" % name,
"//lib/guice",
"//lib/jgit/org.eclipse.jgit:jgit",
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java
index 794956f..6cfc583 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticQueryAccountsTest.java
@@ -15,10 +15,10 @@
package com.google.gerrit.elasticsearch;
import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
-import com.google.gerrit.server.query.IndexConfig;
import com.google.gerrit.server.query.account.AbstractQueryAccountsTest;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
import com.google.inject.Guice;
import com.google.inject.Injector;
import java.util.concurrent.ExecutionException;
@@ -42,7 +42,6 @@
return;
}
nodeInfo = ElasticTestUtils.startElasticsearchNode();
- ElasticTestUtils.createAllIndexes(nodeInfo);
}
@AfterClass
@@ -54,11 +53,14 @@
}
}
+ private String testName() {
+ return testName.getMethodName().toLowerCase() + "_";
+ }
+
@After
public void cleanupIndex() {
if (nodeInfo != null) {
- ElasticTestUtils.deleteAllIndexes(nodeInfo);
- ElasticTestUtils.createAllIndexes(nodeInfo);
+ ElasticTestUtils.deleteAllIndexes(nodeInfo, testName());
}
}
@@ -66,7 +68,9 @@
protected Injector createInjector() {
Config elasticsearchConfig = new Config(config);
InMemoryModule.setDefaults(elasticsearchConfig);
- ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port);
+ String indicesPrefix = testName();
+ ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+ ElasticTestUtils.createAllIndexes(nodeInfo, indicesPrefix);
return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
}
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
index bc6c853..5949c06 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticQueryChangesTest.java
@@ -15,11 +15,11 @@
package com.google.gerrit.elasticsearch;
import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
-import com.google.gerrit.server.query.IndexConfig;
import com.google.gerrit.server.query.change.AbstractQueryChangesTest;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
import com.google.gerrit.testing.InMemoryRepositoryManager.Repo;
+import com.google.gerrit.testing.IndexConfig;
import com.google.inject.Guice;
import com.google.inject.Injector;
import java.util.concurrent.ExecutionException;
@@ -45,16 +45,6 @@
return;
}
nodeInfo = ElasticTestUtils.startElasticsearchNode();
-
- ElasticTestUtils.createAllIndexes(nodeInfo);
- }
-
- @After
- public void cleanupIndex() {
- if (nodeInfo != null) {
- ElasticTestUtils.deleteAllIndexes(nodeInfo);
- ElasticTestUtils.createAllIndexes(nodeInfo);
- }
}
@AfterClass
@@ -66,11 +56,24 @@
}
}
+ private String testName() {
+ return testName.getMethodName().toLowerCase() + "_";
+ }
+
+ @After
+ public void cleanupIndex() {
+ if (nodeInfo != null) {
+ ElasticTestUtils.deleteAllIndexes(nodeInfo, testName());
+ }
+ }
+
@Override
protected Injector createInjector() {
Config elasticsearchConfig = new Config(config);
InMemoryModule.setDefaults(elasticsearchConfig);
- ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port);
+ String indicesPrefix = testName();
+ ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+ ElasticTestUtils.createAllIndexes(nodeInfo, indicesPrefix);
return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java
index 9659c9e..a1c331d 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticQueryGroupsTest.java
@@ -15,10 +15,10 @@
package com.google.gerrit.elasticsearch;
import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
-import com.google.gerrit.server.query.IndexConfig;
import com.google.gerrit.server.query.group.AbstractQueryGroupsTest;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
import com.google.inject.Guice;
import com.google.inject.Injector;
import java.util.concurrent.ExecutionException;
@@ -42,7 +42,6 @@
return;
}
nodeInfo = ElasticTestUtils.startElasticsearchNode();
- ElasticTestUtils.createAllIndexes(nodeInfo);
}
@AfterClass
@@ -54,11 +53,14 @@
}
}
+ private String testName() {
+ return testName.getMethodName().toLowerCase() + "_";
+ }
+
@After
public void cleanupIndex() {
if (nodeInfo != null) {
- ElasticTestUtils.deleteAllIndexes(nodeInfo);
- ElasticTestUtils.createAllIndexes(nodeInfo);
+ ElasticTestUtils.deleteAllIndexes(nodeInfo, testName());
}
}
@@ -66,7 +68,9 @@
protected Injector createInjector() {
Config elasticsearchConfig = new Config(config);
InMemoryModule.setDefaults(elasticsearchConfig);
- ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port);
+ String indicesPrefix = testName();
+ ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+ ElasticTestUtils.createAllIndexes(nodeInfo, indicesPrefix);
return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
}
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java
index 66a6aab..07fbf56 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticQueryProjectsTest.java
@@ -15,10 +15,10 @@
package com.google.gerrit.elasticsearch;
import com.google.gerrit.elasticsearch.ElasticTestUtils.ElasticNodeInfo;
-import com.google.gerrit.server.query.IndexConfig;
import com.google.gerrit.server.query.project.AbstractQueryProjectsTest;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
import com.google.inject.Guice;
import com.google.inject.Injector;
import java.util.concurrent.ExecutionException;
@@ -42,7 +42,6 @@
return;
}
nodeInfo = ElasticTestUtils.startElasticsearchNode();
- ElasticTestUtils.createAllIndexes(nodeInfo);
}
@AfterClass
@@ -54,11 +53,14 @@
}
}
+ private String testName() {
+ return testName.getMethodName().toLowerCase() + "_";
+ }
+
@After
public void cleanupIndex() {
if (nodeInfo != null) {
- ElasticTestUtils.deleteAllIndexes(nodeInfo);
- ElasticTestUtils.createAllIndexes(nodeInfo);
+ ElasticTestUtils.deleteAllIndexes(nodeInfo, testName());
}
}
@@ -66,7 +68,9 @@
protected Injector createInjector() {
Config elasticsearchConfig = new Config(config);
InMemoryModule.setDefaults(elasticsearchConfig);
- ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port);
+ String indicesPrefix = testName();
+ ElasticTestUtils.configure(elasticsearchConfig, nodeInfo.port, indicesPrefix);
+ ElasticTestUtils.createAllIndexes(nodeInfo, indicesPrefix);
return Guice.createInjector(new InMemoryModule(elasticsearchConfig, notesMigration));
}
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
index 1936707..ed21e6e 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticTestUtils.java
@@ -70,11 +70,12 @@
}
}
- static void configure(Config config, String port) {
+ static void configure(Config config, String port, String prefix) {
config.setEnum("index", null, "type", IndexType.ELASTICSEARCH);
config.setString("elasticsearch", "test", "protocol", "http");
config.setString("elasticsearch", "test", "hostname", "localhost");
config.setString("elasticsearch", "test", "port", port);
+ config.setString("elasticsearch", null, "prefix", prefix);
}
static ElasticNodeInfo startElasticsearchNode() throws InterruptedException, ExecutionException {
@@ -109,8 +110,46 @@
return new ElasticNodeInfo(node, elasticDir, getHttpPort(node));
}
- static void deleteAllIndexes(ElasticNodeInfo nodeInfo) {
- nodeInfo.node.client().admin().indices().prepareDelete("_all").execute().actionGet();
+ static void deleteAllIndexes(ElasticNodeInfo nodeInfo, String prefix) {
+ Schema<ChangeData> changeSchema = ChangeSchemaDefinitions.INSTANCE.getLatest();
+ nodeInfo
+ .node
+ .client()
+ .admin()
+ .indices()
+ .prepareDelete(String.format("%s%s_%04d", prefix, CHANGES, changeSchema.getVersion()))
+ .execute()
+ .actionGet();
+
+ Schema<AccountState> accountSchema = AccountSchemaDefinitions.INSTANCE.getLatest();
+ nodeInfo
+ .node
+ .client()
+ .admin()
+ .indices()
+ .prepareDelete(String.format("%s%s_%04d", prefix, ACCOUNTS, accountSchema.getVersion()))
+ .execute()
+ .actionGet();
+
+ Schema<InternalGroup> groupSchema = GroupSchemaDefinitions.INSTANCE.getLatest();
+ nodeInfo
+ .node
+ .client()
+ .admin()
+ .indices()
+ .prepareDelete(String.format("%s%s_%04d", prefix, GROUPS, groupSchema.getVersion()))
+ .execute()
+ .actionGet();
+
+ Schema<ProjectData> projectSchema = ProjectSchemaDefinitions.INSTANCE.getLatest();
+ nodeInfo
+ .node
+ .client()
+ .admin()
+ .indices()
+ .prepareDelete(String.format("%s%s_%04d", prefix, PROJECTS, projectSchema.getVersion()))
+ .execute()
+ .actionGet();
}
static class NodeInfo {
@@ -121,7 +160,7 @@
Map<String, NodeInfo> nodes;
}
- static void createAllIndexes(ElasticNodeInfo nodeInfo) {
+ static void createAllIndexes(ElasticNodeInfo nodeInfo, String prefix) {
Schema<ChangeData> changeSchema = ChangeSchemaDefinitions.INSTANCE.getLatest();
ChangeMapping openChangesMapping = new ChangeMapping(changeSchema);
ChangeMapping closedChangesMapping = new ChangeMapping(changeSchema);
@@ -132,7 +171,7 @@
.client()
.admin()
.indices()
- .prepareCreate(String.format("%s_%04d", CHANGES, changeSchema.getVersion()))
+ .prepareCreate(String.format("%s%s_%04d", prefix, CHANGES, changeSchema.getVersion()))
.addMapping(OPEN_CHANGES, gson.toJson(openChangesMapping))
.addMapping(CLOSED_CHANGES, gson.toJson(closedChangesMapping))
.execute()
@@ -145,7 +184,7 @@
.client()
.admin()
.indices()
- .prepareCreate(String.format("%s_%04d", ACCOUNTS, accountSchema.getVersion()))
+ .prepareCreate(String.format("%s%s_%04d", prefix, ACCOUNTS, accountSchema.getVersion()))
.addMapping(ElasticAccountIndex.ACCOUNTS, gson.toJson(accountMapping))
.execute()
.actionGet();
@@ -157,7 +196,7 @@
.client()
.admin()
.indices()
- .prepareCreate(String.format("%s_%04d", GROUPS, groupSchema.getVersion()))
+ .prepareCreate(String.format("%s%s_%04d", prefix, GROUPS, groupSchema.getVersion()))
.addMapping(ElasticGroupIndex.GROUPS, gson.toJson(groupMapping))
.execute()
.actionGet();
@@ -169,7 +208,7 @@
.client()
.admin()
.indices()
- .prepareCreate(String.format("%s_%04d", PROJECTS, projectSchema.getVersion()))
+ .prepareCreate(String.format("%s%s_%04d", prefix, PROJECTS, projectSchema.getVersion()))
.addMapping(ElasticProjectIndex.PROJECTS, gson.toJson(projectMapping))
.execute()
.actionGet();
diff --git a/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java b/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
index b6ff156..117e474 100644
--- a/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
+++ b/javatests/com/google/gerrit/extensions/registration/DynamicSetTest.java
@@ -34,7 +34,7 @@
@Test
public void containsWithEmpty() throws Exception {
DynamicSet<Integer> ds = new DynamicSet<>();
- assertThat(ds.contains(2)).isFalse(); //See above comment about ds.contains
+ assertThat(ds.contains(2)).isFalse(); // See above comment about ds.contains
}
@Test
@@ -42,7 +42,7 @@
DynamicSet<Integer> ds = new DynamicSet<>();
ds.add(2);
- assertThat(ds.contains(2)).isTrue(); //See above comment about ds.contains
+ assertThat(ds.contains(2)).isTrue(); // See above comment about ds.contains
}
@Test
@@ -50,7 +50,7 @@
DynamicSet<Integer> ds = new DynamicSet<>();
ds.add(2);
- assertThat(ds.contains(3)).isFalse(); //See above comment about ds.contains
+ assertThat(ds.contains(3)).isFalse(); // See above comment about ds.contains
}
@Test
@@ -59,7 +59,7 @@
ds.add(2);
ds.add(4);
- assertThat(ds.contains(4)).isTrue(); //See above comment about ds.contains
+ assertThat(ds.contains(4)).isTrue(); // See above comment about ds.contains
}
@Test
@@ -68,7 +68,7 @@
ds.add(2);
ds.add(4);
- assertThat(ds.contains(3)).isFalse(); //See above comment about ds.contains
+ assertThat(ds.contains(3)).isFalse(); // See above comment about ds.contains
}
@Test
@@ -82,12 +82,12 @@
ds.add(6);
// At first, 4 is contained.
- assertThat(ds.contains(4)).isTrue(); //See above comment about ds.contains
+ assertThat(ds.contains(4)).isTrue(); // See above comment about ds.contains
// Then we remove 4.
handle.remove();
// And now 4 should no longer be contained.
- assertThat(ds.contains(4)).isFalse(); //See above comment about ds.contains
+ assertThat(ds.contains(4)).isFalse(); // See above comment about ds.contains
}
}
diff --git a/javatests/com/google/gerrit/git/testing/BUILD b/javatests/com/google/gerrit/git/testing/BUILD
new file mode 100644
index 0000000..13eb5bf
--- /dev/null
+++ b/javatests/com/google/gerrit/git/testing/BUILD
@@ -0,0 +1,10 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+ name = "testing_tests",
+ srcs = glob(["*.java"]),
+ deps = [
+ "//java/com/google/gerrit/git/testing",
+ "//lib:truth",
+ ],
+)
diff --git a/javatests/com/google/gerrit/git/testing/PushResultSubjectTest.java b/javatests/com/google/gerrit/git/testing/PushResultSubjectTest.java
new file mode 100644
index 0000000..3bf815b
--- /dev/null
+++ b/javatests/com/google/gerrit/git/testing/PushResultSubjectTest.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.git.testing;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.git.testing.PushResultSubject.parseProcessed;
+import static com.google.gerrit.git.testing.PushResultSubject.trimMessages;
+
+import org.junit.Test;
+
+public class PushResultSubjectTest {
+ @Test
+ public void testTrimMessages() {
+ assertThat(trimMessages(null)).isNull();
+ assertThat(trimMessages("")).isEqualTo("");
+ assertThat(trimMessages(" \n ")).isEqualTo("");
+ assertThat(trimMessages("\n Foo\nBar\n\nProcessing changes: 1, 2, 3 done \n"))
+ .isEqualTo("Foo\nBar");
+ }
+
+ @Test
+ public void testParseProcessed() {
+ assertThat(parseProcessed(null)).isEmpty();
+ assertThat(parseProcessed("some other output")).isEmpty();
+ assertThat(parseProcessed("Processing changes: done\n")).isEmpty();
+ assertThat(parseProcessed("Processing changes: refs: 1, done \n")).containsExactly("refs", 1);
+ assertThat(parseProcessed("Processing changes: new: 1, updated: 2, refs: 3, done \n"))
+ .containsExactly("new", 1, "updated", 2, "refs", 3)
+ .inOrder();
+ assertThat(
+ parseProcessed(
+ "Some\nlonger\nmessage\nProcessing changes: new: 1\r"
+ + "Processing changes: new: 1, updated: 1\r"
+ + "Processing changes: new: 1, updated: 2, done"))
+ .containsExactly("new", 1, "updated", 2)
+ .inOrder();
+
+ // Atypical, but could potentially happen if there is an uncaught exception.
+ assertThat(parseProcessed("Processing changes: refs: 1")).containsExactly("refs", 1);
+ }
+}
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 24d2822..2ddfaa9 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -55,6 +55,7 @@
"//lib:gwtorm",
"//lib:truth-java8-extension",
"//lib/auto:auto-value",
+ "//lib/auto:auto-value-annotations",
"//lib/commons:codec",
"//lib/guice",
"//lib/jgit/org.eclipse.jgit:jgit",
diff --git a/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java b/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
index 87e2e67..80a15a3 100644
--- a/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
+++ b/javatests/com/google/gerrit/server/account/AuthorizedKeysTest.java
@@ -113,30 +113,30 @@
@Test
public void validity() throws Exception {
- AccountSshKey key = new AccountSshKey(new AccountSshKey.Id(accountId, -1), KEY1);
- assertThat(key.isValid()).isFalse();
- key = new AccountSshKey(new AccountSshKey.Id(accountId, 0), KEY1);
- assertThat(key.isValid()).isFalse();
- key = new AccountSshKey(new AccountSshKey.Id(accountId, 1), KEY1);
- assertThat(key.isValid()).isTrue();
+ AccountSshKey key = AccountSshKey.create(accountId, -1, KEY1);
+ assertThat(key.valid()).isFalse();
+ key = AccountSshKey.create(accountId, 0, KEY1);
+ assertThat(key.valid()).isFalse();
+ key = AccountSshKey.create(accountId, 1, KEY1);
+ assertThat(key.valid()).isTrue();
}
@Test
public void getters() throws Exception {
- AccountSshKey key = new AccountSshKey(new AccountSshKey.Id(accountId, 1), KEY1);
- assertThat(key.getSshPublicKey()).isEqualTo(KEY1);
- assertThat(key.getAlgorithm()).isEqualTo(KEY1.split(" ")[0]);
- assertThat(key.getEncodedKey()).isEqualTo(KEY1.split(" ")[1]);
- assertThat(key.getComment()).isEqualTo(KEY1.split(" ")[2]);
+ AccountSshKey key = AccountSshKey.create(accountId, 1, KEY1);
+ assertThat(key.sshPublicKey()).isEqualTo(KEY1);
+ assertThat(key.algorithm()).isEqualTo(KEY1.split(" ")[0]);
+ assertThat(key.encodedKey()).isEqualTo(KEY1.split(" ")[1]);
+ assertThat(key.comment()).isEqualTo(KEY1.split(" ")[2]);
}
@Test
public void keyWithNewLines() throws Exception {
- AccountSshKey key = new AccountSshKey(new AccountSshKey.Id(accountId, 1), KEY1_WITH_NEWLINES);
- assertThat(key.getSshPublicKey()).isEqualTo(KEY1);
- assertThat(key.getAlgorithm()).isEqualTo(KEY1.split(" ")[0]);
- assertThat(key.getEncodedKey()).isEqualTo(KEY1.split(" ")[1]);
- assertThat(key.getComment()).isEqualTo(KEY1.split(" ")[2]);
+ AccountSshKey key = AccountSshKey.create(accountId, 1, KEY1_WITH_NEWLINES);
+ assertThat(key.sshPublicKey()).isEqualTo(KEY1);
+ assertThat(key.algorithm()).isEqualTo(KEY1.split(" ")[0]);
+ assertThat(key.encodedKey()).isEqualTo(KEY1.split(" ")[1]);
+ assertThat(key.comment()).isEqualTo(KEY1.split(" ")[2]);
}
private static String toWindowsLineEndings(String s) {
@@ -157,8 +157,8 @@
int seq = 1;
for (Optional<AccountSshKey> sshKey : parsedKeys) {
if (sshKey.isPresent()) {
- assertThat(sshKey.get().getAccount()).isEqualTo(accountId);
- assertThat(sshKey.get().getKey().get()).isEqualTo(seq);
+ assertThat(sshKey.get().accountId()).isEqualTo(accountId);
+ assertThat(sshKey.get().seq()).isEqualTo(seq);
}
seq++;
}
@@ -170,10 +170,9 @@
* @return the expected line for this key in the authorized_keys file
*/
private static String addKey(List<Optional<AccountSshKey>> keys, String pub) {
- AccountSshKey.Id keyId = new AccountSshKey.Id(new Account.Id(1), keys.size() + 1);
- AccountSshKey key = new AccountSshKey(keyId, pub);
+ AccountSshKey key = AccountSshKey.create(new Account.Id(1), keys.size() + 1, pub);
keys.add(Optional.of(key));
- return key.getSshPublicKey() + "\n";
+ return key.sshPublicKey() + "\n";
}
/**
@@ -182,11 +181,9 @@
* @return the expected line for this key in the authorized_keys file
*/
private static String addInvalidKey(List<Optional<AccountSshKey>> keys, String pub) {
- AccountSshKey.Id keyId = new AccountSshKey.Id(new Account.Id(1), keys.size() + 1);
- AccountSshKey key = new AccountSshKey(keyId, pub);
- key.setInvalid();
+ AccountSshKey key = AccountSshKey.createInvalid(new Account.Id(1), keys.size() + 1, pub);
keys.add(Optional.of(key));
- return AuthorizedKeys.INVALID_KEY_COMMENT_PREFIX + key.getSshPublicKey() + "\n";
+ return AuthorizedKeys.INVALID_KEY_COMMENT_PREFIX + key.sshPublicKey() + "\n";
}
/**
diff --git a/javatests/com/google/gerrit/server/cache/BUILD b/javatests/com/google/gerrit/server/cache/BUILD
new file mode 100644
index 0000000..6eca172
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/BUILD
@@ -0,0 +1,12 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+
+junit_tests(
+ name = "tests",
+ srcs = ["PerThreadCacheTest.java"],
+ deps = [
+ "//java/com/google/gerrit/server",
+ "//lib:guava",
+ "//lib:junit",
+ "//lib:truth",
+ ],
+)
diff --git a/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
new file mode 100644
index 0000000..ae5e911
--- /dev/null
+++ b/javatests/com/google/gerrit/server/cache/PerThreadCacheTest.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.function.Supplier;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class PerThreadCacheTest {
+ @Rule public ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void key_respectsClass() {
+ assertThat(PerThreadCache.Key.create(String.class))
+ .isEqualTo(PerThreadCache.Key.create(String.class));
+ assertThat(PerThreadCache.Key.create(String.class))
+ .isNotEqualTo(PerThreadCache.Key.create(Integer.class));
+ }
+
+ @Test
+ public void key_respectsIdentifiers() {
+ assertThat(PerThreadCache.Key.create(String.class, "id1"))
+ .isEqualTo(PerThreadCache.Key.create(String.class, "id1"));
+ assertThat(PerThreadCache.Key.create(String.class, "id1"))
+ .isNotEqualTo(PerThreadCache.Key.create(String.class, "id2"));
+ }
+
+ @Test
+ public void endToEndCache() {
+ try (PerThreadCache ignored = PerThreadCache.create()) {
+ PerThreadCache cache = PerThreadCache.get();
+ PerThreadCache.Key<String> key1 = PerThreadCache.Key.create(String.class);
+
+ String value1 = cache.get(key1, () -> "value1");
+ assertThat(value1).isEqualTo("value1");
+
+ Supplier<String> neverCalled =
+ () -> {
+ throw new IllegalStateException("this method must not be called");
+ };
+ assertThat(cache.get(key1, neverCalled)).isEqualTo("value1");
+ }
+ }
+
+ @Test
+ public void cleanUp() {
+ PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class);
+ try (PerThreadCache ignored = PerThreadCache.create()) {
+ PerThreadCache cache = PerThreadCache.get();
+ String value1 = cache.get(key, () -> "value1");
+ assertThat(value1).isEqualTo("value1");
+ }
+
+ // Create a second cache and assert that it is not connected to the first one.
+ // This ensures that the cleanup is actually working.
+ try (PerThreadCache ignored = PerThreadCache.create()) {
+ PerThreadCache cache = PerThreadCache.get();
+ String value1 = cache.get(key, () -> "value2");
+ assertThat(value1).isEqualTo("value2");
+ }
+ }
+
+ @Test
+ public void doubleInstantiationFails() {
+ try (PerThreadCache ignored = PerThreadCache.create()) {
+ exception.expect(IllegalStateException.class);
+ exception.expectMessage("called create() twice on the same request");
+ PerThreadCache.create();
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
index 577d931..935dfc6 100644
--- a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
+++ b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
@@ -20,11 +20,15 @@
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.restapi.config.ListCapabilities;
import com.google.gerrit.server.restapi.config.ListCapabilities.CapabilityInfo;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
+import com.google.inject.Singleton;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
@@ -48,6 +52,7 @@
return "Print Hello";
}
});
+ bind(PermissionBackend.class).to(FakePermissionBackend.class);
}
};
injector = Guice.createInjector(mod);
@@ -68,4 +73,27 @@
assertThat(m.get(pluginCapability).id).isEqualTo(pluginCapability);
assertThat(m.get(pluginCapability).name).isEqualTo("Print Hello");
}
+
+ @Singleton
+ private static class FakePermissionBackend extends PermissionBackend {
+ @Override
+ public WithUser currentUser() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public WithUser user(CurrentUser user) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public WithUser absentUser(Id user) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean usesDefaultCapabilities() {
+ return true;
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
index 5772a80..d242962 100644
--- a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
+++ b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
@@ -51,6 +51,11 @@
}
@Override
+ public Object getCacheKey() {
+ return new Object();
+ }
+
+ @Override
public boolean isIdentifiedUser() {
return true;
}
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
index 95ac3e3..8e8a0ea 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -93,7 +93,7 @@
}
@Test
- public void storedSubmitRecordsWithRequirements() {
+ public void storedSubmitRecordsWithRequirement() {
SubmitRecord r =
record(
SubmitRecord.Status.OK,
@@ -101,10 +101,27 @@
label(SubmitRecord.Label.Status.OK, "Label-2", 1));
SubmitRequirement sr =
- new SubmitRequirement(
- "short reason",
- "Full reason can be a long string with special symbols like < > \\ / ; :",
- null);
+ SubmitRequirement.builder()
+ .setType("short_type")
+ .setFallbackText("Fallback text may contain special symbols like < > \\ / ; :")
+ .addCustomValue("custom_data", "my value")
+ .build();
+ r.requirements = Collections.singletonList(sr);
+
+ assertStoredRecordRoundTrip(r);
+ }
+
+ @Test
+ public void storedSubmitRequirementWithoutCustomData() {
+ SubmitRecord r =
+ record(
+ SubmitRecord.Status.OK,
+ label(SubmitRecord.Label.Status.MAY, "Label-1", null),
+ label(SubmitRecord.Label.Status.OK, "Label-2", 1));
+
+ // Doesn't have any custom data value
+ SubmitRequirement sr =
+ SubmitRequirement.builder().setFallbackText("short_type").setType("ci_status").build();
r.requirements = Collections.singletonList(sr);
assertStoredRecordRoundTrip(r);
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
index 9cf013b..53994a6 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -70,7 +70,8 @@
Predicate<ChangeData> out = rewrite(in);
assertThat(AndChangeSource.class).isSameAs(out.getClass());
assertThat(out.getChildren())
- .containsExactly(query(ChangeStatusPredicate.open()), in)
+ .containsExactly(
+ query(Predicate.or(ChangeStatusPredicate.open(), ChangeStatusPredicate.closed())), in)
.inOrder();
}
@@ -86,7 +87,8 @@
Predicate<ChangeData> out = rewrite(in);
assertThat(AndChangeSource.class).isSameAs(out.getClass());
assertThat(out.getChildren())
- .containsExactly(query(ChangeStatusPredicate.open()), in)
+ .containsExactly(
+ query(Predicate.or(ChangeStatusPredicate.open(), ChangeStatusPredicate.closed())), in)
.inOrder();
}
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index 08ff40b..c677be5 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -123,7 +123,7 @@
private String systemTimeZone;
@Before
- public void setUp() throws Exception {
+ public void setUpTestEnvironment() throws Exception {
setTimeForTesting();
serverIdent = new PersonIdent("Gerrit Server", "noreply@gerrit.com", TimeUtil.nowTs(), TZ);
diff --git a/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java b/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java
index 2b660ed..ef80d7e 100644
--- a/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java
+++ b/javatests/com/google/gerrit/server/patch/IntraLineLoaderTest.java
@@ -77,14 +77,14 @@
String b = "multi\nlinemulti\nline\n";
assertThat(intraline(a, b)).isEqualTo(wordEdit(10, 10, 6, 16));
// better would be:
- //assertThat(intraline(a, b)).isEqualTo(wordEdit(6, 6, 6, 16));
+ // assertThat(intraline(a, b)).isEqualTo(wordEdit(6, 6, 6, 16));
// or the equivalent:
- //assertThat(intraline(a, b)).isEqualTo(ref()
+ // assertThat(intraline(a, b)).isEqualTo(ref()
// .common("multi\n").insert("linemulti\n").common("line\n").edits
- //);
+ // );
}
- //TODO: expected failure
+ // TODO: expected failure
// the current code does not work on the first line
// and the insert marker is in the wrong location
@Test(expected = AssertionError.class)
@@ -95,7 +95,7 @@
.isEqualTo(ref().insert(" ").common(" abc\n").insert(" ").common(" def\n").edits);
}
- //TODO: expected failure
+ // TODO: expected failure
// the current code does not work on the first line
@Test(expected = AssertionError.class)
public void preferDeleteAtLineBreak() throws Exception {
diff --git a/javatests/com/google/gerrit/server/permissions/RefPermissionTest.java b/javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java
similarity index 71%
rename from javatests/com/google/gerrit/server/permissions/RefPermissionTest.java
rename to javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java
index f665fdc..305e81b 100644
--- a/javatests/com/google/gerrit/server/permissions/RefPermissionTest.java
+++ b/javatests/com/google/gerrit/server/permissions/DefaultPermissionsMappingTest.java
@@ -15,16 +15,17 @@
package com.google.gerrit.server.permissions;
import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.server.permissions.DefaultPermissionMappings.refPermission;
import com.google.gerrit.common.data.Permission;
import org.junit.Test;
-public class RefPermissionTest {
+public class DefaultPermissionsMappingTest {
@Test
- public void fromName() {
- assertThat(RefPermission.fromName("doesnotexist")).isEmpty();
- assertThat(RefPermission.fromName("")).isEmpty();
- assertThat(RefPermission.fromName(Permission.VIEW_PRIVATE_CHANGES))
+ public void stringToRefPermission() {
+ assertThat(refPermission("doesnotexist")).isEmpty();
+ assertThat(refPermission("")).isEmpty();
+ assertThat(refPermission(Permission.VIEW_PRIVATE_CHANGES))
.hasValue(RefPermission.READ_PRIVATE_CHANGES);
}
}
diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
index 9722184..a14895b 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -37,6 +37,7 @@
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
@@ -258,6 +259,12 @@
@Override
public void evict(Project.NameKey p) {}
+
+ @Override
+ public ProjectState checkedGet(Project.NameKey projectName, boolean strict)
+ throws Exception {
+ return all.get(projectName);
+ }
};
Injector injector = Guice.createInjector(new InMemoryModule());
@@ -976,7 +983,8 @@
return user(local, null, memberOf);
}
- private ProjectControl user(ProjectConfig local, String name, AccountGroup.UUID... memberOf) {
+ private ProjectControl user(
+ ProjectConfig local, @Nullable String name, AccountGroup.UUID... memberOf) {
return new ProjectControl(
Collections.<AccountGroup.UUID>emptySet(),
Collections.<AccountGroup.UUID>emptySet(),
@@ -994,10 +1002,10 @@
}
private class MockUser extends CurrentUser {
- private final String username;
+ @Nullable private final String username;
private final GroupMembership groups;
- MockUser(String name, AccountGroup.UUID[] groupId) {
+ MockUser(@Nullable String name, AccountGroup.UUID[] groupId) {
username = name;
ArrayList<AccountGroup.UUID> groupIds = Lists.newArrayList(groupId);
groupIds.add(REGISTERED_USERS);
@@ -1011,8 +1019,13 @@
}
@Override
+ public Object getCacheKey() {
+ return new Object();
+ }
+
+ @Override
public Optional<String> getUserName() {
- return Optional.of(username);
+ return Optional.ofNullable(username);
}
}
}
diff --git a/javatests/com/google/gerrit/server/query/BUILD b/javatests/com/google/gerrit/server/query/BUILD
deleted file mode 100644
index 96201d2..0000000
--- a/javatests/com/google/gerrit/server/query/BUILD
+++ /dev/null
@@ -1,10 +0,0 @@
-load("//tools/bzl:junit.bzl", "junit_tests")
-
-java_library(
- name = "index-config",
- srcs = glob(["*.java"]),
- visibility = ["//visibility:public"],
- deps = [
- "//lib/jgit/org.eclipse.jgit:jgit",
- ],
-)
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 77b7729..d1c7477 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -601,8 +601,9 @@
List<AccountExternalIdInfo> externalIdInfos = gApi.accounts().self().getExternalIds();
List<ByteArrayWrapper> blobs = new ArrayList<>();
for (AccountExternalIdInfo info : externalIdInfos) {
- blobs.add(
- new ByteArrayWrapper(externalIds.get(ExternalId.Key.parse(info.identity)).toByteArray()));
+ Optional<ExternalId> extId = externalIds.get(ExternalId.Key.parse(info.identity));
+ assertThat(extId).isPresent();
+ blobs.add(new ByteArrayWrapper(extId.get().toByteArray()));
}
assertThat(rawFields.get().getValue(AccountField.EXTERNAL_ID_STATE)).hasSize(blobs.size());
assertThat(
diff --git a/javatests/com/google/gerrit/server/query/account/BUILD b/javatests/com/google/gerrit/server/query/account/BUILD
index 497fc22..c352f43 100644
--- a/javatests/com/google/gerrit/server/query/account/BUILD
+++ b/javatests/com/google/gerrit/server/query/account/BUILD
@@ -35,7 +35,6 @@
":abstract_query_tests",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/testing:gerrit-test-util",
- "//javatests/com/google/gerrit/server/query:index-config",
"//lib/guice",
"//lib/jgit/org.eclipse.jgit:jgit",
],
diff --git a/javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java
index da4b0d5..660c1d8 100644
--- a/javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java
@@ -15,9 +15,9 @@
package com.google.gerrit.server.query.account;
import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
-import com.google.gerrit.server.query.IndexConfig;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
import com.google.gerrit.testing.IndexVersions;
import com.google.inject.Guice;
import com.google.inject.Injector;
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 1b56d60..525e030 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -18,6 +18,11 @@
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.project.testing.Util.allow;
+import static com.google.gerrit.server.project.testing.Util.category;
+import static com.google.gerrit.server.project.testing.Util.value;
+import static com.google.gerrit.server.project.testing.Util.verified;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
@@ -36,6 +41,8 @@
import com.google.common.truth.ThrowableSubject;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AssigneeInput;
@@ -50,6 +57,7 @@
import com.google.gerrit.extensions.api.changes.StarsInput;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.api.projects.ProjectInput;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.client.ReviewerState;
@@ -100,6 +108,7 @@
import com.google.gerrit.server.notedb.NoteDbChangeState;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.schema.SchemaCreator;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.util.ManualRequestContext;
@@ -173,6 +182,7 @@
@Inject protected ThreadLocalRequestContext requestContext;
@Inject protected ProjectCache projectCache;
@Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
+ @Inject protected IdentifiedUser.GenericFactory identifiedUserFactory;
// Only for use in setting up/tearing down injector; other users should use schemaFactory.
@Inject private InMemoryDatabase inMemoryDatabase;
@@ -366,6 +376,7 @@
assertQuery("status:pe", expected);
assertQuery("status:pen", expected);
assertQuery("is:open", expected);
+ assertQuery("is:pending", expected);
}
@Test
@@ -621,13 +632,15 @@
public void byCommit() throws Exception {
TestRepository<Repo> repo = createProject("repo");
ChangeInserter ins = newChange(repo);
- insert(repo, ins);
+ Change change = insert(repo, ins);
String sha = ins.getCommitId().name();
assertQuery("0000000000000000000000000000000000000000");
+ assertQuery("commit:0000000000000000000000000000000000000000");
for (int i = 0; i <= 36; i++) {
String q = sha.substring(0, 40 - i);
- assertQuery(q, ins.getChange());
+ assertQuery(q, change);
+ assertQuery("commit:" + q, change);
}
}
@@ -639,6 +652,7 @@
accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
Change change2 = insert(repo, newChange(repo), user2);
+ assertQuery("is:owner", change1);
assertQuery("owner:" + userId.get(), change1);
assertQuery("owner:" + user2, change2);
@@ -734,9 +748,14 @@
Account.Id user2 =
accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
Change change2 = insert(repo, newChange(repo), user2);
+ Change change3 = insert(repo, newChange(repo), user2);
+ gApi.changes().id(change3.getId().get()).current().review(ReviewInput.approve());
+ gApi.changes().id(change3.getId().get()).current().submit();
assertQuery("ownerin:Administrators", change1);
- assertQuery("ownerin:\"Registered Users\"", change2, change1);
+ assertQuery("ownerin:\"Registered Users\"", change3, change2, change1);
+ assertQuery("ownerin:\"Registered Users\" project:repo", change3, change2, change1);
+ assertQuery("ownerin:\"Registered Users\" status:merged", change3);
}
@Test
@@ -753,6 +772,17 @@
}
@Test
+ public void byParentProject() throws Exception {
+ TestRepository<Repo> repo1 = createProject("repo1");
+ TestRepository<Repo> repo2 = createProject("repo2", "repo1");
+ Change change1 = insert(repo1, newChange(repo1));
+ Change change2 = insert(repo2, newChange(repo2));
+
+ assertQuery("parentproject:repo1", change2, change1);
+ assertQuery("parentproject:repo2", change2);
+ }
+
+ @Test
public void byProjectPrefix() throws Exception {
TestRepository<Repo> repo1 = createProject("repo1");
TestRepository<Repo> repo2 = createProject("repo2");
@@ -939,6 +969,68 @@
}
@Test
+ public void byLabelMulti() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Project.NameKey project =
+ new Project.NameKey(repo.getRepository().getDescription().getRepositoryName());
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+
+ LabelType verified =
+ category("Verified", value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
+ cfg.getLabelSections().put(verified.getName(), verified);
+
+ String heads = RefNames.REFS_HEADS + "*";
+ allow(cfg, Permission.forLabel(verified().getName()), -1, 1, REGISTERED_USERS, heads);
+
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+ cfg.commit(md);
+ }
+ projectCache.evict(cfg.getProject());
+
+ ReviewInput reviewVerified = new ReviewInput().label("Verified", 1);
+ ChangeInserter ins = newChange(repo, null, null, null, null, false);
+ ChangeInserter ins2 = newChange(repo, null, null, null, null, false);
+ ChangeInserter ins3 = newChange(repo, null, null, null, null, false);
+ ChangeInserter ins4 = newChange(repo, null, null, null, null, false);
+ ChangeInserter ins5 = newChange(repo, null, null, null, null, false);
+
+ // CR+1
+ Change reviewCRplus1 = insert(repo, ins);
+ gApi.changes().id(reviewCRplus1.getId().get()).current().review(ReviewInput.recommend());
+
+ // CR+2
+ Change reviewCRplus2 = insert(repo, ins2);
+ gApi.changes().id(reviewCRplus2.getId().get()).current().review(ReviewInput.approve());
+
+ // CR+1 VR+1
+ Change reviewCRplus1VRplus1 = insert(repo, ins3);
+ gApi.changes().id(reviewCRplus1VRplus1.getId().get()).current().review(ReviewInput.recommend());
+ gApi.changes().id(reviewCRplus1VRplus1.getId().get()).current().review(reviewVerified);
+
+ // CR+2 VR+1
+ Change reviewCRplus2VRplus1 = insert(repo, ins4);
+ gApi.changes().id(reviewCRplus2VRplus1.getId().get()).current().review(ReviewInput.approve());
+ gApi.changes().id(reviewCRplus2VRplus1.getId().get()).current().review(reviewVerified);
+
+ // VR+1
+ Change reviewVRplus1 = insert(repo, ins5);
+ gApi.changes().id(reviewVRplus1.getId().get()).current().review(reviewVerified);
+
+ assertQuery("label:Code-Review=+1", reviewCRplus1VRplus1, reviewCRplus1);
+ assertQuery(
+ "label:Code-Review>=+1",
+ reviewCRplus2VRplus1,
+ reviewCRplus1VRplus1,
+ reviewCRplus2,
+ reviewCRplus1);
+ assertQuery("label:Code-Review>=+2", reviewCRplus2VRplus1, reviewCRplus2);
+
+ assertQuery(
+ "label:Code-Review>=+1 label:Verified=+1", reviewCRplus2VRplus1, reviewCRplus1VRplus1);
+ assertQuery("label:Code-Review>=+2 label:Verified=+1", reviewCRplus2VRplus1);
+ }
+
+ @Test
public void byLabelNotOwner() throws Exception {
TestRepository<Repo> repo = createProject("repo");
ChangeInserter ins = newChange(repo, null, null, null, null, false);
@@ -1288,7 +1380,7 @@
}
@Test
- public void byBefore() throws Exception {
+ public void byBeforeUntil() throws Exception {
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
TestRepository<Repo> repo = createProject("repo");
@@ -1297,20 +1389,22 @@
Change change2 = insert(repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
TestTimeUtil.setClockStep(0, MILLISECONDS);
- assertQuery("before:2009-09-29");
- assertQuery("before:2009-09-30");
- assertQuery("before:\"2009-09-30 16:59:00 -0400\"");
- assertQuery("before:\"2009-09-30 20:59:00 -0000\"");
- assertQuery("before:\"2009-09-30 20:59:00\"");
- assertQuery("before:\"2009-09-30 17:02:00 -0400\"", change1);
- assertQuery("before:\"2009-10-01 21:02:00 -0000\"", change1);
- assertQuery("before:\"2009-10-01 21:02:00\"", change1);
- assertQuery("before:2009-10-01", change1);
- assertQuery("before:2009-10-03", change2, change1);
+ for (String predicate : Lists.newArrayList("before:", "until:")) {
+ assertQuery(predicate + "2009-09-29");
+ assertQuery(predicate + "2009-09-30");
+ assertQuery(predicate + "\"2009-09-30 16:59:00 -0400\"");
+ assertQuery(predicate + "\"2009-09-30 20:59:00 -0000\"");
+ assertQuery(predicate + "\"2009-09-30 20:59:00\"");
+ assertQuery(predicate + "\"2009-09-30 17:02:00 -0400\"", change1);
+ assertQuery(predicate + "\"2009-10-01 21:02:00 -0000\"", change1);
+ assertQuery(predicate + "\"2009-10-01 21:02:00\"", change1);
+ assertQuery(predicate + "2009-10-01", change1);
+ assertQuery(predicate + "2009-10-03", change2, change1);
+ }
}
@Test
- public void byAfter() throws Exception {
+ public void byAfterSince() throws Exception {
long thirtyHoursInMs = MILLISECONDS.convert(30, HOURS);
resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
TestRepository<Repo> repo = createProject("repo");
@@ -1319,11 +1413,13 @@
Change change2 = insert(repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
TestTimeUtil.setClockStep(0, MILLISECONDS);
- assertQuery("after:2009-10-03");
- assertQuery("after:\"2009-10-01 20:59:59 -0400\"", change2);
- assertQuery("after:\"2009-10-01 20:59:59 -0000\"", change2);
- assertQuery("after:2009-10-01", change2);
- assertQuery("after:2009-09-30", change2, change1);
+ for (String predicate : Lists.newArrayList("after:", "since:")) {
+ assertQuery(predicate + "2009-10-03");
+ assertQuery(predicate + "\"2009-10-01 20:59:59 -0400\"", change2);
+ assertQuery(predicate + "\"2009-10-01 20:59:59 -0000\"", change2);
+ assertQuery(predicate + "2009-10-01", change2);
+ assertQuery(predicate + "2009-09-30", change2, change1);
+ }
}
@Test
@@ -1373,13 +1469,13 @@
assertQuery("deleted:<=0", change1);
- for (String str : Lists.newArrayList("delta", "size")) {
- assertQuery(str + ":<2");
- assertQuery(str + ":3", change1);
- assertQuery(str + ":>2", change1);
- assertQuery(str + ":>=3", change1);
- assertQuery(str + ":<3", change2);
- assertQuery(str + ":<=2", change2);
+ for (String str : Lists.newArrayList("delta:", "size:")) {
+ assertQuery(str + "<2");
+ assertQuery(str + "3", change1);
+ assertQuery(str + ">2", change1);
+ assertQuery(str + ">=3", change1);
+ assertQuery(str + "<3", change2);
+ assertQuery(str + "<=2", change2);
}
}
@@ -1489,6 +1585,28 @@
}
@Test
+ public void visible() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(repo, newChange(repo));
+ Change change2 = insert(repo, newChange(repo));
+
+ gApi.changes().id(change2.getChangeId()).setPrivate(true, "private");
+
+ String q = "project:repo";
+ assertQuery(q, change2, change1);
+
+ // Second user cannot see first user's private change.
+ Account.Id user2 =
+ accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+ assertQuery(q + " visibleto:" + user2.get(), change1);
+
+ requestContext.setContext(
+ newRequestContext(
+ accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId()));
+ assertQuery("is:visible", change1);
+ }
+
+ @Test
public void byCommentBy() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChange(repo));
@@ -1718,6 +1836,26 @@
}
@Test
+ public void mergeable() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ RevCommit commit1 = repo.parseBody(repo.commit().add("file1", "contents1").create());
+ RevCommit commit2 = repo.parseBody(repo.commit().add("file1", "contents2").create());
+ Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+
+ assertQuery("conflicts:" + change1.getId().get(), change2);
+ assertQuery("conflicts:" + change2.getId().get(), change1);
+ assertQuery("is:mergeable", change2, change1);
+
+ gApi.changes().id(change1.getChangeId()).revision("current").review(ReviewInput.approve());
+ gApi.changes().id(change1.getChangeId()).revision("current").submit();
+
+ assertQuery("status:open conflicts:" + change2.getId().get());
+ assertQuery("status:open is:mergeable");
+ assertQuery("status:open -is:mergeable", change2);
+ }
+
+ @Test
public void reviewedBy() throws Exception {
resetTimeWithClockStep(2, MINUTES);
TestRepository<Repo> repo = createProject("repo");
@@ -1764,6 +1902,7 @@
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChange(repo));
Change change2 = insert(repo, newChange(repo));
+ Change change3 = insert(repo, newChange(repo));
insert(repo, newChange(repo));
AddReviewerInput rin = new AddReviewerInput();
@@ -1776,16 +1915,92 @@
rin.state = ReviewerState.CC;
gApi.changes().id(change2.getId().get()).addReviewer(rin);
+ assertQuery("is:reviewer");
+ assertQuery("reviewer:self");
+ gApi.changes().id(change3.getChangeId()).revision("current").review(ReviewInput.recommend());
+ assertQuery("is:reviewer", change3);
+ assertQuery("reviewer:self", change3);
+
+ requestContext.setContext(newRequestContext(user1));
if (notesMigration.readChanges()) {
assertQuery("reviewer:" + user1, change1);
assertQuery("cc:" + user1, change2);
+ assertQuery("is:cc", change2);
+ assertQuery("cc:self", change2);
} else {
assertQuery("reviewer:" + user1, change2, change1);
assertQuery("cc:" + user1);
+ assertQuery("is:cc");
+ assertQuery("cc:self");
}
}
@Test
+ public void byReviewed() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Account.Id otherUser =
+ accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+ Change change1 = insert(repo, newChange(repo));
+ Change change2 = insert(repo, newChange(repo));
+
+ assertQuery("is:reviewed");
+ assertQuery("status:reviewed");
+ assertQuery("-is:reviewed", change2, change1);
+ assertQuery("-status:reviewed", change2, change1);
+
+ requestContext.setContext(newRequestContext(otherUser));
+ gApi.changes().id(change1.getChangeId()).current().review(ReviewInput.recommend());
+
+ assertQuery("is:reviewed", change1);
+ assertQuery("status:reviewed", change1);
+ assertQuery("-is:reviewed", change2);
+ assertQuery("-status:reviewed", change2);
+ }
+
+ @Test
+ public void reviewerin() throws Exception {
+ Account.Id user1 = accountManager.authenticate(AuthRequest.forUser("user1")).getAccountId();
+ Account.Id user2 = accountManager.authenticate(AuthRequest.forUser("user2")).getAccountId();
+ TestRepository<Repo> repo = createProject("repo");
+
+ Change change1 = insert(repo, newChange(repo));
+ Change change2 = insert(repo, newChange(repo));
+ insert(repo, newChange(repo));
+
+ AddReviewerInput rin = new AddReviewerInput();
+ rin.reviewer = user1.toString();
+ rin.state = ReviewerState.REVIEWER;
+ gApi.changes().id(change1.getId().get()).addReviewer(rin);
+
+ rin = new AddReviewerInput();
+ rin.reviewer = user2.toString();
+ rin.state = ReviewerState.REVIEWER;
+ gApi.changes().id(change2.getId().get()).addReviewer(rin);
+
+ String group = gApi.groups().create("foo").get().name;
+ gApi.groups().id(group).addMembers(user2.toString());
+
+ List<String> members =
+ gApi.groups()
+ .id(group)
+ .members()
+ .stream()
+ .map(a -> a._accountId.toString())
+ .collect(toList());
+ assertThat(members).contains(user2.toString());
+
+ assertQuery("reviewerin:\"Registered Users\"", change2, change1);
+ assertQuery("reviewerin:" + group, change2);
+
+ gApi.changes().id(change2.getId().get()).current().review(ReviewInput.approve());
+ gApi.changes().id(change2.getId().get()).current().submit();
+
+ assertQuery("reviewerin:" + group, change2);
+ assertQuery("project:repo reviewerin:" + group, change2);
+ assertQuery("status:merged reviewerin:" + group, change2);
+ }
+
+ @Test
public void reviewerAndCcByEmail() throws Exception {
assume().that(notesMigration.readChanges()).isTrue();
@@ -1899,6 +2114,10 @@
// NEED records don't have associated users.
assertQuery("label:CodE-RevieW=need,user1");
assertQuery("label:CodE-RevieW=need,user");
+
+ gApi.changes().id(change1.getId().get()).current().submit();
+ assertQuery("submittable:ok");
+ assertQuery("submittable:closed", change1);
}
@Test
@@ -2207,6 +2426,28 @@
}
@Test
+ public void trackingid() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ RevCommit commit1 =
+ repo.parseBody(repo.commit().message("Change one\n\nBug:QUERY123").create());
+ Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ RevCommit commit2 =
+ repo.parseBody(repo.commit().message("Change two\n\nFeature:QUERY456").create());
+ Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+
+ assertQuery("tr:QUERY123", change1);
+ assertQuery("bug:QUERY123", change1);
+ assertQuery("tr:QUERY456", change2);
+ assertQuery("bug:QUERY456", change2);
+ assertQuery("tr:QUERY-123");
+ assertQuery("bug:QUERY-123");
+ assertQuery("tr:QUERY12");
+ assertQuery("bug:QUERY12");
+ assertQuery("tr:QUERY789");
+ assertQuery("bug:QUERY789");
+ }
+
+ @Test
public void selfAndMe() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChange(repo));
@@ -2553,6 +2794,89 @@
mergedOwned);
}
+ @Test
+ public void assignee() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(repo, newChange(repo));
+ Change change2 = insert(repo, newChange(repo));
+
+ AssigneeInput input = new AssigneeInput();
+ input.assignee = user.getUserName().get();
+ gApi.changes().id(change1.getChangeId()).setAssignee(input);
+
+ assertQuery("is:assigned", change1);
+ assertQuery("-is:assigned", change2);
+ assertQuery("is:unassigned", change2);
+ assertQuery("-is:unassigned", change1);
+ assertQuery("assignee:" + user.getUserName().get(), change1);
+ assertQuery("-assignee:" + user.getUserName().get(), change2);
+ }
+
+ @Test
+ public void userDestination() throws Exception {
+ TestRepository<Repo> repo1 = createProject("repo1");
+ Change change1 = insert(repo1, newChange(repo1));
+ TestRepository<Repo> repo2 = createProject("repo2");
+ Change change2 = insert(repo2, newChange(repo2));
+
+ assertThatQueryException("destination:foo")
+ .hasMessageThat()
+ .isEqualTo("Unknown named destination: foo");
+
+ String destination1 = "refs/heads/master\trepo1";
+ String destination2 = "refs/heads/master\trepo2";
+ String destination3 = "refs/heads/master\trepo1\nrefs/heads/master\trepo2";
+ String destination4 = "refs/heads/master\trepo3";
+ String destination5 = "refs/heads/other\trepo1";
+
+ TestRepository<Repo> allUsers = new TestRepository<>(repoManager.openRepository(allUsersName));
+ String refsUsers = RefNames.refsUsers(userId);
+ allUsers.branch(refsUsers).commit().add("destinations/destination1", destination1).create();
+ allUsers.branch(refsUsers).commit().add("destinations/destination2", destination2).create();
+ allUsers.branch(refsUsers).commit().add("destinations/destination3", destination3).create();
+ allUsers.branch(refsUsers).commit().add("destinations/destination4", destination4).create();
+ allUsers.branch(refsUsers).commit().add("destinations/destination5", destination5).create();
+
+ Ref userRef = allUsers.getRepository().exactRef(refsUsers);
+ assertThat(userRef).isNotNull();
+
+ assertQuery("destination:destination1", change1);
+ assertQuery("destination:destination2", change2);
+ assertQuery("destination:destination3", change2, change1);
+ assertQuery("destination:destination4");
+ assertQuery("destination:destination5");
+ }
+
+ @Test
+ public void userQuery() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(repo, newChange(repo));
+ Change change2 = insert(repo, newChangeForBranch(repo, "stable"));
+
+ String queries =
+ "query1\tproject:repo\n"
+ + "query2\tproject:repo status:open\n"
+ + "query3\tproject:repo branch:stable\n"
+ + "query4\tproject:repo branch:other";
+
+ TestRepository<Repo> allUsers = new TestRepository<>(repoManager.openRepository(allUsersName));
+ String refsUsers = RefNames.refsUsers(userId);
+ allUsers.branch(refsUsers).commit().add("queries", queries).create();
+
+ Ref userRef = allUsers.getRepository().exactRef(refsUsers);
+ assertThat(userRef).isNotNull();
+
+ assertThatQueryException("query:foo").hasMessageThat().isEqualTo("Unknown named query: foo");
+
+ assertQuery("query:query1", change2, change1);
+ assertQuery("query:query2", change2, change1);
+ gApi.changes().id(change1.getChangeId()).revision("current").review(ReviewInput.approve());
+ gApi.changes().id(change1.getChangeId()).revision("current").submit();
+ assertQuery("query:query2", change2);
+ assertQuery("query:query3", change2);
+ assertQuery("query:query4");
+ }
+
protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception {
return newChange(repo, null, null, null, null, false);
}
@@ -2678,6 +3002,14 @@
return new TestRepository<>(repoManager.openRepository(new Project.NameKey(name)));
}
+ protected TestRepository<Repo> createProject(String name, String parent) throws Exception {
+ ProjectInput input = new ProjectInput();
+ input.name = name;
+ input.parent = parent;
+ gApi.projects().create(input).get();
+ return new TestRepository<>(repoManager.openRepository(new Project.NameKey(name)));
+ }
+
protected QueryRequest newQuery(Object query) {
return gApi.changes().query(query.toString());
}
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index 5ade4ef..66c825c 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -16,6 +16,7 @@
"//java/com/google/gerrit/lifecycle",
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server/project/testing:project-test-util",
"//java/com/google/gerrit/server/schema",
"//java/com/google/gerrit/testing:gerrit-test-util",
"//lib:gwtorm",
@@ -40,7 +41,6 @@
"//java/com/google/gerrit/reviewdb:server",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/testing:gerrit-test-util",
- "//javatests/com/google/gerrit/server/query:index-config",
"//lib:gwtorm",
"//lib:truth",
"//lib/guice",
diff --git a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
index e0ddc4c..5ee3aa4 100644
--- a/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -17,10 +17,10 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
-import com.google.gerrit.server.query.IndexConfig;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
import com.google.gerrit.testing.InMemoryRepositoryManager.Repo;
+import com.google.gerrit.testing.IndexConfig;
import com.google.gerrit.testing.IndexVersions;
import com.google.inject.Guice;
import com.google.inject.Injector;
diff --git a/javatests/com/google/gerrit/server/query/group/BUILD b/javatests/com/google/gerrit/server/query/group/BUILD
index 7f14fe3..01a54a3 100644
--- a/javatests/com/google/gerrit/server/query/group/BUILD
+++ b/javatests/com/google/gerrit/server/query/group/BUILD
@@ -34,7 +34,6 @@
":abstract_query_tests",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/testing:gerrit-test-util",
- "//javatests/com/google/gerrit/server/query:index-config",
"//lib/guice",
"//lib/jgit/org.eclipse.jgit:jgit",
],
diff --git a/javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java
index be231a3..83835c1 100644
--- a/javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java
@@ -15,9 +15,9 @@
package com.google.gerrit.server.query.group;
import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
-import com.google.gerrit.server.query.IndexConfig;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
import com.google.gerrit.testing.IndexVersions;
import com.google.inject.Guice;
import com.google.inject.Injector;
diff --git a/javatests/com/google/gerrit/server/query/project/BUILD b/javatests/com/google/gerrit/server/query/project/BUILD
index 4ad9e73..ac2692b 100644
--- a/javatests/com/google/gerrit/server/query/project/BUILD
+++ b/javatests/com/google/gerrit/server/query/project/BUILD
@@ -33,7 +33,6 @@
"//java/com/google/gerrit/index/project",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/testing:gerrit-test-util",
- "//javatests/com/google/gerrit/server/query:index-config",
"//lib/guice",
"//lib/jgit/org.eclipse.jgit:jgit",
],
diff --git a/javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java
index 1cf09d8..42964fa 100644
--- a/javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/LuceneQueryProjectsTest.java
@@ -15,9 +15,9 @@
package com.google.gerrit.server.query.project;
import com.google.gerrit.index.project.ProjectSchemaDefinitions;
-import com.google.gerrit.server.query.IndexConfig;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
import com.google.gerrit.testing.IndexVersions;
import com.google.inject.Guice;
import com.google.inject.Injector;
diff --git a/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java b/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java
index 9cd57e0..6020325 100644
--- a/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java
+++ b/javatests/com/google/gerrit/server/schema/Schema_166_to_167_WithGroupsInReviewDbTest.java
@@ -23,10 +23,7 @@
import static com.google.gerrit.truth.OptionalSubject.assertThat;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupDescription.Basic;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.accounts.AccountInput;
@@ -52,7 +49,6 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.GroupMembership;
import com.google.gerrit.server.account.GroupUUID;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerId;
@@ -65,7 +61,7 @@
import com.google.gerrit.server.group.db.GroupNameNotes;
import com.google.gerrit.server.group.db.GroupsConsistencyChecker;
import com.google.gerrit.server.group.testing.InternalGroupSubject;
-import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.group.testing.TestGroupBackend;
import com.google.gerrit.testing.InMemoryTestEnvironment;
import com.google.gerrit.testing.TestTimeUtil;
import com.google.gerrit.testing.TestTimeUtil.TempClockStep;
@@ -84,11 +80,9 @@
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Optional;
-import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@@ -591,9 +585,9 @@
public void logFormatWithExternalGroup() throws Exception {
AccountGroup group = createInReviewDb("group");
- backends.add(new TestGroupBackend());
- AccountGroup.UUID subgroupUuid = TestGroupBackend.createUuuid("foo");
-
+ TestGroupBackend testGroupBackend = new TestGroupBackend();
+ backends.add(testGroupBackend);
+ AccountGroup.UUID subgroupUuid = testGroupBackend.create("test").getGroupUUID();
assertThat(groupBackend.handles(subgroupUuid)).isTrue();
addSubgroupsInReviewDb(group.getId(), subgroupUuid);
@@ -637,10 +631,10 @@
"Update group\n"
+ "\n"
+ "Add-group: "
- + TestGroupBackend.PREFIX
- + "foo <"
- + TestGroupBackend.PREFIX
- + "foo>");
+ + subgroupUuid.get()
+ + " <"
+ + subgroupUuid.get()
+ + ">");
assertThat(log.get(2)).author().name().isEqualTo(currentUser.getName());
assertThat(log.get(2)).author().email().isEqualTo(currentUser.getAccountId() + "@" + serverId);
assertThat(log.get(2)).committer().hasSameDateAs(log.get(2).author);
@@ -1128,77 +1122,4 @@
groupInfo.options.visibleToAll = group.isVisibleToAll() ? true : null;
return groupInfo;
}
-
- private static class TestGroupBackend implements GroupBackend {
- static final String PREFIX = "testbackend:";
-
- static AccountGroup.UUID createUuuid(String name) {
- return new AccountGroup.UUID(PREFIX + name);
- }
-
- @Override
- public Collection<GroupReference> suggest(String name, ProjectState project) {
- return ImmutableSet.of();
- }
-
- @Override
- public GroupMembership membershipsOf(IdentifiedUser user) {
- return new GroupMembership() {
- @Override
- public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> groupIds) {
- return ImmutableSet.of();
- }
-
- @Override
- public Set<AccountGroup.UUID> getKnownGroups() {
- return ImmutableSet.of();
- }
-
- @Override
- public boolean containsAnyOf(Iterable<AccountGroup.UUID> groupIds) {
- return false;
- }
-
- @Override
- public boolean contains(AccountGroup.UUID groupId) {
- return false;
- }
- };
- }
-
- @Override
- public boolean isVisibleToAll(AccountGroup.UUID uuid) {
- return false;
- }
-
- @Override
- public boolean handles(AccountGroup.UUID uuid) {
- return uuid.get().startsWith(PREFIX);
- }
-
- @Override
- public Basic get(AccountGroup.UUID uuid) {
- return new GroupDescription.Basic() {
- @Override
- public AccountGroup.UUID getGroupUUID() {
- return uuid;
- }
-
- @Override
- public String getName() {
- return uuid.get().substring(PREFIX.length());
- }
-
- @Override
- public String getEmailAddress() {
- return null;
- }
-
- @Override
- public String getUrl() {
- return null;
- }
- };
- }
- }
}
diff --git a/lib/auto/BUILD b/lib/auto/BUILD
index 569398e..89adbde 100644
--- a/lib/auto/BUILD
+++ b/lib/auto/BUILD
@@ -1,13 +1,19 @@
java_plugin(
name = "auto-annotation-plugin",
processor_class = "com.google.auto.value.processor.AutoAnnotationProcessor",
- deps = ["@auto_value//jar"],
+ deps = [
+ "@auto_value//jar",
+ "@auto_value_annotations//jar",
+ ],
)
java_plugin(
name = "auto-value-plugin",
processor_class = "com.google.auto.value.processor.AutoValueProcessor",
- deps = ["@auto_value//jar"],
+ deps = [
+ "@auto_value//jar",
+ "@auto_value_annotations//jar",
+ ],
)
java_library(
@@ -20,3 +26,14 @@
visibility = ["//visibility:public"],
exports = ["@auto_value//jar"],
)
+
+java_library(
+ name = "auto-value-annotations",
+ data = ["//lib:LICENSE-Apache2.0"],
+ exported_plugins = [
+ ":auto-annotation-plugin",
+ ":auto-value-plugin",
+ ],
+ visibility = ["//visibility:public"],
+ exports = ["@auto_value_annotations//jar"],
+)
diff --git a/lib/elasticsearch/BUILD b/lib/elasticsearch/BUILD
index 18c62af..13c033e 100644
--- a/lib/elasticsearch/BUILD
+++ b/lib/elasticsearch/BUILD
@@ -7,7 +7,6 @@
runtime_deps = [
":compress-lzf",
":hppc",
- ":jna",
":joda-time",
":jsr166e",
":netty",
@@ -15,39 +14,16 @@
"//lib/jackson:jackson-core",
"//lib/jackson:jackson-dataformat-cbor",
"//lib/jackson:jackson-dataformat-smile",
- "//lib/lucene:lucene-codecs",
"//lib/lucene:lucene-highlighter",
"//lib/lucene:lucene-join",
"//lib/lucene:lucene-memory",
"//lib/lucene:lucene-queries",
- "//lib/lucene:lucene-sandbox",
"//lib/lucene:lucene-spatial",
"//lib/lucene:lucene-suggest",
],
)
java_library(
- name = "jest-common",
- data = ["//lib:LICENSE-Apache2.0"],
- exports = ["@jest_common//jar"],
-)
-
-java_library(
- name = "jest",
- data = ["//lib:LICENSE-Apache2.0"],
- exports = ["@jest//jar"],
- runtime_deps = [
- ":elasticsearch",
- ":jest-common",
- "//lib/commons:lang3",
- "//lib/httpcomponents:httpasyncclient",
- "//lib/httpcomponents:httpclient",
- "//lib/httpcomponents:httpcore-nio",
- "//lib/httpcomponents:httpcore-niossl",
- ],
-)
-
-java_library(
name = "joda-time",
data = ["//lib:LICENSE-Apache2.0"],
exports = ["@joda_time//jar"],
@@ -94,9 +70,3 @@
visibility = ["//lib/elasticsearch:__pkg__"],
exports = ["@t_digest//jar"],
)
-
-java_library(
- name = "jna",
- data = ["//lib:LICENSE-Apache2.0"],
- exports = ["@jna//jar"],
-)
diff --git a/lib/httpcomponents/BUILD b/lib/httpcomponents/BUILD
index 2b2cc6f..6e8fcd8 100644
--- a/lib/httpcomponents/BUILD
+++ b/lib/httpcomponents/BUILD
@@ -45,9 +45,3 @@
data = ["//lib:LICENSE-Apache2.0"],
exports = ["@httpcore_nio//jar"],
)
-
-java_library(
- name = "httpcore-niossl",
- data = ["//lib:LICENSE-Apache2.0"],
- exports = ["@httpcore_niossl//jar"],
-)
diff --git a/lib/jackson/BUILD b/lib/jackson/BUILD
index 4847371..8ade0cf 100644
--- a/lib/jackson/BUILD
+++ b/lib/jackson/BUILD
@@ -9,13 +9,13 @@
)
java_library(
- name = "jackson-dataformat-smile",
- data = ["//lib:LICENSE-Apache2.0"],
- exports = ["@jackson_dataformat_smile//jar"],
-)
-
-java_library(
name = "jackson-dataformat-cbor",
data = ["//lib:LICENSE-Apache2.0"],
exports = ["@jackson_dataformat_cbor//jar"],
)
+
+java_library(
+ name = "jackson-dataformat-smile",
+ data = ["//lib:LICENSE-Apache2.0"],
+ exports = ["@jackson_dataformat_smile//jar"],
+)
diff --git a/lib/jest/BUILD b/lib/jest/BUILD
new file mode 100644
index 0000000..169f271
--- /dev/null
+++ b/lib/jest/BUILD
@@ -0,0 +1,23 @@
+package(default_visibility = ["//visibility:public"])
+
+java_library(
+ name = "jest-common",
+ data = ["//lib:LICENSE-Apache2.0"],
+ visibility = ["//visibility:public"],
+ exports = ["@jest_common//jar"],
+ runtime_deps = [
+ "//lib/commons:lang3",
+ ],
+)
+
+java_library(
+ name = "jest",
+ data = ["//lib:LICENSE-Apache2.0"],
+ visibility = ["//visibility:public"],
+ exports = ["@jest//jar"],
+ runtime_deps = [
+ "//lib/httpcomponents:httpasyncclient",
+ "//lib/httpcomponents:httpclient",
+ "//lib/httpcomponents:httpcore-nio",
+ ],
+)
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index cf389850..749fec9 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,6 +1,6 @@
load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_LOCAL", "MAVEN_CENTRAL", "maven_jar")
-_JGIT_VERS = "4.11.0.201803080745-r.2-g61e4f1665"
+_JGIT_VERS = "4.11.0.201803080745-r.93-gcbb2e65db"
_DOC_VERS = "4.11.0.201803080745-r" # Set to _JGIT_VERS unless using a snapshot
@@ -26,28 +26,28 @@
name = "jgit_lib",
artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
repository = _JGIT_REPO,
- sha1 = "38489eca0a4308087081d07774af86aa6a50b2ab",
- src_sha1 = "e43c58829c72b5b18e16d1b2bbd1396ddd93098f",
+ sha1 = "265a39c017ecfeed7e992b6aaa336e515bf6e157",
+ src_sha1 = "e9d801e17afe71cdd5ade84ab41ff0110c3f28fd",
unsign = True,
)
maven_jar(
name = "jgit_servlet",
artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
repository = _JGIT_REPO,
- sha1 = "f5be45e4f97f0bf0825e4ff8fcb2f47588dd7e92",
+ sha1 = "0d68f62286b5db759fdbeb122c789db1f833a06a",
unsign = True,
)
maven_jar(
name = "jgit_archive",
artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
repository = _JGIT_REPO,
- sha1 = "f202f169b2e3a50be90b4123baa941136eda3ed6",
+ sha1 = "4cc3ed2c42ee63593fd1b16215fcf13eeefb833e",
)
maven_jar(
name = "jgit_junit",
artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
repository = _JGIT_REPO,
- sha1 = "dd9f7e4cc41b4f47591ce51c4752ccfef012c553",
+ sha1 = "6f1bcc9ac22b31b5a6e1e68c08283850108b900c",
unsign = True,
)
diff --git a/lib/js/BUILD b/lib/js/BUILD
index 8a7986e..706c472 100644
--- a/lib/js/BUILD
+++ b/lib/js/BUILD
@@ -24,6 +24,12 @@
define_bower_components()
+js_component(
+ name = "highlightjs",
+ srcs = ["//lib/highlightjs:highlight.min.js"],
+ license = "//lib:LICENSE-highlightjs",
+)
+
filegroup(
name = "highlightjs_files",
srcs = ["//lib/highlightjs:highlight.min.js"],
diff --git a/lib/lucene/BUILD b/lib/lucene/BUILD
index bbf43a6..6590af4 100644
--- a/lib/lucene/BUILD
+++ b/lib/lucene/BUILD
@@ -23,13 +23,6 @@
)
java_library(
- name = "lucene-codecs",
- data = ["//lib:LICENSE-Apache2.0"],
- visibility = ["//visibility:public"],
- exports = ["@lucene_codecs//jar"],
-)
-
-java_library(
name = "lucene-core",
data = ["//lib:LICENSE-Apache2.0"],
visibility = ["//visibility:public"],
@@ -71,12 +64,6 @@
)
java_library(
- name = "lucene-sandbox",
- data = ["//lib:LICENSE-Apache2.0"],
- exports = ["@lucene_sandbox//jar"],
-)
-
-java_library(
name = "lucene-spatial",
data = ["//lib:LICENSE-Apache2.0"],
exports = ["@lucene_spatial//jar"],
diff --git a/plugins/replication b/plugins/replication
index a62d1c6..5e91925 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit a62d1c601e6c7fb669c847a0e1843e6f60cd1cb2
+Subproject commit 5e91925cfd391898e8e33fd149b9e1a115dafee4
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index b338cbf..fddfc57 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -11,6 +11,9 @@
"//lib/js:ba-linkify",
"//lib/js:es6-promise",
"//lib/js:fetch",
+ # Although highlightjs is inserted separately in the UI zip, it's used
+ # by local development servers (e.g. --polygerrit-dev or run-server.sh).
+ "//lib/js:highlightjs",
"//lib/js:iron-a11y-keys-behavior",
"//lib/js:iron-autogrow-textarea",
"//lib/js:iron-dropdown",
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 713b073..8f946c5 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -68,6 +68,12 @@
PATH=$PATH:/usr/local/go/bin
```
+Install the go Soy template library:
+
+```
+go get "github.com/robfig/soy"
+```
+
### Running the server
To test the local UI against gerrit-review.googlesource.com:
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 593a34b..c735746 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -15,7 +15,6 @@
],
exclude = [
"bower_components/**",
- "index.html",
"test/**",
"**/*_test.html",
],
@@ -66,6 +65,8 @@
("tar -hcf- $(locations :pg_code) |" +
" tar --strip-components=2 -C $$TMP/ -xf-"),
"cd $$TMP",
+ "TZ=UTC",
+ "export TZ",
"find . -exec touch -t 198001010000 '{}' ';'",
"zip -rq $$ROOT/$@ *",
]),
@@ -158,7 +159,6 @@
],
exclude = [
"bower_components/**",
- "index.html",
"test/**",
"**/*_test.html",
],
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html
new file mode 100644
index 0000000..db11937
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html
@@ -0,0 +1,206 @@
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<script>
+(function(window) {
+ 'use strict';
+
+ const ACCOUNT_CAPABILITIES = ['createProject', 'createGroup', 'viewPlugins'];
+
+ const ADMIN_LINKS = [{
+ name: 'Repositories',
+ noBaseUrl: true,
+ url: '/admin/repos',
+ view: 'gr-repo-list',
+ viewableToAll: true,
+ }, {
+ name: 'Groups',
+ section: 'Groups',
+ noBaseUrl: true,
+ url: '/admin/groups',
+ view: 'gr-admin-group-list',
+ }, {
+ name: 'Plugins',
+ capability: 'viewPlugins',
+ section: 'Plugins',
+ noBaseUrl: true,
+ url: '/admin/plugins',
+ view: 'gr-plugin-list',
+ }];
+
+ window.Gerrit = window.Gerrit || {};
+
+ /** @polymerBehavior Gerrit.AdminNavBehavior */
+ Gerrit.AdminNavBehavior = {
+ /**
+ * @param {!Object} account
+ * @param {!Function} getAccountCapabilities
+ * @param {!Function} getAdminMenuLinks
+ * Possible aguments in options:
+ * repoName?: string
+ * groupId?: string,
+ * groupName?: string,
+ * groupIsInternal?: boolean,
+ * isAdmin?: boolean,
+ * groupOwner?: boolean,
+ * @param {!Object=} opt_options
+ * @return {Promise<!Object>}
+ */
+ getAdminLinks(account, getAccountCapabilities, getAdminMenuLinks,
+ opt_options) {
+ if (!account) {
+ return Promise.resolve(this._filterLinks(link => link.viewableToAll,
+ getAdminMenuLinks, opt_options));
+ }
+ return getAccountCapabilities(ACCOUNT_CAPABILITIES)
+ .then(capabilities => {
+ return this._filterLinks(link => {
+ return !link.capability ||
+ capabilities.hasOwnProperty(link.capability);
+ }, getAdminMenuLinks, opt_options);
+ });
+ },
+
+ /**
+ * @param {!Function} filterFn
+ * @param {!Function} getAdminMenuLinks
+ * Possible aguments in options:
+ * repoName?: string
+ * groupId?: string,
+ * groupName?: string,
+ * groupIsInternal?: boolean,
+ * isAdmin?: boolean,
+ * groupOwner?: boolean,
+ * @param {!Object|undefined} opt_options
+ * @return {Promise<!Object>}
+ */
+ _filterLinks(filterFn, getAdminMenuLinks, opt_options) {
+ let links = ADMIN_LINKS.slice(0);
+ let expandedSection;
+
+ const isExernalLink = link => link.url[0] !== '/';
+
+ // Append top-level links that are defined by plugins.
+ links.push(...getAdminMenuLinks().map(link => ({
+ url: link.url,
+ name: link.text,
+ noBaseUrl: !isExernalLink(link),
+ view: null,
+ viewableToAll: true,
+ target: isExernalLink(link) ? '_blank' : null,
+ })));
+
+ links = links.filter(filterFn);
+
+ const filteredLinks = [];
+ const repoName = opt_options && opt_options.repoName;
+ const groupId = opt_options && opt_options.groupId;
+ const groupName = opt_options && opt_options.groupName;
+ const groupIsInternal = opt_options && opt_options.groupIsInternal;
+ const isAdmin = opt_options && opt_options.isAdmin;
+ const groupOwner = opt_options && opt_options.groupOwner;
+
+ // Don't bother to get sub-navigation items if only the top level links
+ // are needed. This is used by the main header dropdown.
+ if (!repoName && !groupId) { return {links, expandedSection}; }
+
+ // Otherwise determine the full set of links and return both the full
+ // set in addition to the subsection that should be displayed if it
+ // exists.
+ for (const link of links) {
+ const linkCopy = Object.assign({}, link);
+ if (linkCopy.name === 'Repositories' && repoName) {
+ linkCopy.subsection = this.getRepoSubsections(repoName);
+ expandedSection = linkCopy.subsection;
+ } else if (linkCopy.name === 'Groups' && groupId && groupName) {
+ linkCopy.subsection = this.getGroupSubsections(groupId, groupName,
+ groupIsInternal, isAdmin, groupOwner);
+ expandedSection = linkCopy.subsection;
+ }
+ filteredLinks.push(linkCopy);
+ }
+ return {links: filteredLinks, expandedSection};
+ },
+
+ getGroupSubsections(groupId, groupName, groupIsInternal, isAdmin,
+ groupOwner) {
+ const subsection = {
+ name: groupName,
+ view: Gerrit.Nav.View.GROUP,
+ url: Gerrit.Nav.getUrlForGroup(groupId),
+ children: [],
+ };
+ if (groupIsInternal) {
+ subsection.children.push({
+ name: 'Members',
+ detailType: Gerrit.Nav.GroupDetailView.MEMBERS,
+ view: Gerrit.Nav.View.GROUP,
+ url: Gerrit.Nav.getUrlForGroupMembers(groupId),
+ });
+ }
+ if (groupIsInternal && (isAdmin || groupOwner)) {
+ subsection.children.push(
+ {
+ name: 'Audit Log',
+ detailType: Gerrit.Nav.GroupDetailView.LOG,
+ view: Gerrit.Nav.View.GROUP,
+ url: Gerrit.Nav.getUrlForGroupLog(groupId),
+ }
+ );
+ }
+ return subsection;
+ },
+
+ getRepoSubsections(repoName) {
+ return {
+ name: repoName,
+ view: Gerrit.Nav.View.REPO,
+ url: Gerrit.Nav.getUrlForRepo(repoName),
+ children: [{
+ name: 'Access',
+ view: Gerrit.Nav.View.REPO,
+ detailType: Gerrit.Nav.RepoDetailView.ACCESS,
+ url: Gerrit.Nav.getUrlForRepoAccess(repoName),
+ },
+ {
+ name: 'Commands',
+ view: Gerrit.Nav.View.REPO,
+ detailType: Gerrit.Nav.RepoDetailView.COMMANDS,
+ url: Gerrit.Nav.getUrlForRepoCommands(repoName),
+ },
+ {
+ name: 'Branches',
+ view: Gerrit.Nav.View.REPO,
+ detailType: Gerrit.Nav.RepoDetailView.BRANCHES,
+ url: Gerrit.Nav.getUrlForRepoBranches(repoName),
+ },
+ {
+ name: 'Tags',
+ view: Gerrit.Nav.View.REPO,
+ detailType: Gerrit.Nav.RepoDetailView.TAGS,
+ url: Gerrit.Nav.getUrlForRepoTags(repoName),
+ },
+ {
+ name: 'Dashboards',
+ view: Gerrit.Nav.View.REPO,
+ detailType: Gerrit.Nav.RepoDetailView.DASHBOARDS,
+ url: Gerrit.Nav.getUrlForRepoDashboards(repoName),
+ }],
+ };
+ },
+ };
+})(window);
+</script>
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
new file mode 100644
index 0000000..a1902cd
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior_test.html
@@ -0,0 +1,311 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>keyboard-shortcut-behavior</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../test/common-test-setup.html"/>
+<link rel="import" href="gr-admin-nav-behavior.html">
+
+<test-fixture id="basic">
+ <template>
+ <test-element></test-element>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-admin-nav-behavior tests', () => {
+ let element;
+ let sandbox;
+ let capabilityStub;
+ let menuLinkStub;
+
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'test-element',
+ behaviors: [
+ Gerrit.AdminNavBehavior,
+ ],
+ });
+ });
+
+ setup(() => {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ capabilityStub = sinon.stub();
+ menuLinkStub = sinon.stub().returns([]);
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ const testAdminLinks = (account, options, expected, done) => {
+ element.getAdminLinks(account,
+ capabilityStub,
+ menuLinkStub,
+ options)
+ .then(res => {
+ assert.equal(expected.totalLength, res.links.length);
+ assert.equal(res.links[0].name, 'Repositories');
+ // Repos
+ if (expected.groupListShown) {
+ assert.equal(res.links[1].name, 'Groups');
+ }
+
+ if (expected.pluginListShown) {
+ assert.equal(res.links[2].name, 'Plugins');
+ assert.isNotOk(res.links[2].subsection);
+ }
+
+ if (expected.projectPageShown) {
+ assert.isOk(res.links[0].subsection);
+ assert.equal(res.links[0].subsection.children.length, 5);
+ } else {
+ assert.isNotOk(res.links[0].subsection);
+ }
+ // Groups
+ if (expected.groupPageShown) {
+ assert.isOk(res.links[1].subsection);
+ assert.equal(res.links[1].subsection.children.length,
+ expected.groupSubpageLength);
+ } else if ( expected.totalLength > 1) {
+ assert.isNotOk(res.links[1].subsection);
+ }
+
+ if (expected.pluginGeneratedLinks) {
+ for (const link of expected.pluginGeneratedLinks) {
+ const linkMatch = res.links.find(l => {
+ return (l.url === link.url && l.name === link.text);
+ });
+ assert.isTrue(!!linkMatch);
+
+ // External links should open in new tab.
+ if (link.url[0] !== '/') {
+ assert.equal(linkMatch.target, '_blank');
+ } else {
+ assert.isNotOk(linkMatch.target);
+ }
+ }
+ }
+
+ // Current section
+ if (expected.projectPageShown || expected.groupPageShown) {
+ assert.isOk(res.expandedSection);
+ assert.isOk(res.expandedSection.children);
+ } else {
+ assert.isNotOk(res.expandedSection);
+ }
+ if (expected.projectPageShown) {
+ assert.equal(res.expandedSection.name, 'my-repo');
+ assert.equal(res.expandedSection.children.length, 5);
+ } else if (expected.groupPageShown) {
+ assert.equal(res.expandedSection.name, 'my-group');
+ assert.equal(res.expandedSection.children.length,
+ expected.groupSubpageLength);
+ }
+ done();
+ });
+ };
+
+ suite('logged out', () => {
+ let account;
+ let expected;
+
+ setup(() => {
+ expected = {
+ groupListShown: false,
+ groupPageShown: false,
+ pluginListShown: false,
+ };
+ });
+
+ test('without a specific repo or group', done => {
+ let options;
+ expected = Object.assign(expected, {
+ totalLength: 1,
+ projectPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('with a repo', done => {
+ const options = {repoName: 'my-repo'};
+ expected = Object.assign(expected, {
+ totalLength: 1,
+ projectPageShown: true,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('with plugin generated links', done => {
+ let options;
+ const generatedLinks = [
+ {text: 'internal link text', url: '/internal/link/url'},
+ {text: 'external link text', url: 'http://external/link/url'},
+ ];
+ menuLinkStub.returns(generatedLinks);
+ expected = Object.assign(expected, {
+ totalLength: 3,
+ projectPageShown: false,
+ pluginGeneratedLinks: generatedLinks,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+
+ suite('no plugin capability logged in', () => {
+ const account = {
+ name: 'test-user',
+ };
+ let expected;
+
+ setup(() => {
+ expected = {
+ totalLength: 2,
+ pluginListShown: false,
+ };
+ capabilityStub.returns(Promise.resolve({}));
+ });
+
+ test('without a specific project or group', done => {
+ let options;
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupListShown: true,
+ groupPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('with a repo', done => {
+ const account = {
+ name: 'test-user',
+ };
+ const options = {repoName: 'my-repo'};
+ expected = Object.assign(expected, {
+ projectPageShown: true,
+ groupListShown: true,
+ groupPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+
+ suite('view plugin capability logged in', () => {
+ const account = {
+ name: 'test-user',
+ };
+ let expected;
+
+ setup(() => {
+ capabilityStub.returns(Promise.resolve({viewPlugins: true}));
+ expected = {
+ totalLength: 3,
+ groupListShown: true,
+ pluginListShown: true,
+ };
+ });
+
+ test('without a specific repo or group', done => {
+ let options;
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('with a repo', done => {
+ const options = {repoName: 'my-repo'};
+ expected = Object.assign(expected, {
+ projectPageShown: true,
+ groupPageShown: false,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('admin with internal group', done => {
+ const options = {
+ groupId: 'a15262',
+ groupName: 'my-group',
+ groupIsInternal: true,
+ isAdmin: true,
+ groupOwner: false,
+ };
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: true,
+ groupSubpageLength: 2,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('group owner with internal group', done => {
+ const options = {
+ groupId: 'a15262',
+ groupName: 'my-group',
+ groupIsInternal: true,
+ isAdmin: false,
+ groupOwner: true,
+ };
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: true,
+ groupSubpageLength: 2,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('non owner or admin with internal group', done => {
+ const options = {
+ groupId: 'a15262',
+ groupName: 'my-group',
+ groupIsInternal: true,
+ isAdmin: false,
+ groupOwner: false,
+ };
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: true,
+ groupSubpageLength: 1,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+
+ test('admin with external group', done => {
+ const options = {
+ groupId: 'a15262',
+ groupName: 'my-group',
+ groupIsInternal: false,
+ isAdmin: true,
+ groupOwner: true,
+ };
+ expected = Object.assign(expected, {
+ projectPageShown: false,
+ groupPageShown: true,
+ groupSubpageLength: 0,
+ });
+ testAdminLinks(account, options, expected, done);
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
index ead9592..4be674f 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
@@ -151,7 +151,7 @@
if (states.length || !opt_options) { return states; }
// If no missing requirements, either active or ready to submit.
- if (change.submittable) {
+ if (change.submittable && opt_options.submitEnabled) {
states.push('Ready to submit');
} else {
// Otherwise it is active.
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
index 06acd94..d3ce73c 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
@@ -89,7 +89,7 @@
mergeable: true,
};
let statuses = element.changeStatuses(change);
- let statusString = element.changeStatusString(change);
+ const statusString = element.changeStatusString(change);
assert.deepEqual(statuses, []);
assert.equal(statusString, '');
@@ -98,11 +98,15 @@
{includeDerived: true});
assert.deepEqual(statuses, ['Active']);
- // With no missing labels
+ // With no missing labels but no submitEnabled option.
change.submittable = true;
statuses = element.changeStatuses(change,
{includeDerived: true});
- statusString = element.changeStatusString(change);
+ assert.deepEqual(statuses, ['Active']);
+
+ // Without missing labels and enabled submit
+ statuses = element.changeStatuses(change,
+ {includeDerived: true, submitEnabled: true});
assert.deepEqual(statuses, ['Ready to submit']);
change.mergeable = false;
@@ -114,7 +118,7 @@
delete change.mergeable;
change.submittable = true;
statuses = element.changeStatuses(change,
- {includeDerived: true, mergeable: true});
+ {includeDerived: true, mergeable: true, submitEnabled: true});
assert.deepEqual(statuses, ['Ready to submit']);
change.submittable = true;
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
index c996474..3779402 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.html
@@ -36,7 +36,7 @@
margin-bottom: 1em;
}
fieldset {
- border: 1px solid #d1d2d3;
+ border: 1px solid var(--border-color);
}
.name {
align-items: center;
@@ -46,7 +46,7 @@
#deletedContainer {
align-items: center;
background: #f6f6f6;
- border-bottom: 1px dotted #d1d2d3;
+ border-bottom: 1px dotted var(--border-color);
display: flex;
justify-content: space-between;
min-height: 3em;
@@ -122,7 +122,8 @@
labels="[[labels]]"
section="[[section.id]]"
editing="[[editing]]"
- groups="[[groups]]">
+ groups="[[groups]]"
+ on-added-permission-removed="_handleAddedPermissionRemoved">
</gr-permission>
</template>
<div id="addPermission">
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
index b6d2955..4574e11 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
@@ -23,6 +23,11 @@
* @event access-modified
*/
+ /**
+ * Fired when a section that was previously added was removed.
+ * @event added-section-removed
+ */
+
const GLOBAL_NAME = 'GLOBAL_CAPABILITIES';
// The name that gets automatically input when a new reference is added.
@@ -130,6 +135,12 @@
return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : '';
},
+ _handleAddedPermissionRemoved(e) {
+ const index = e.model.index;
+ this._permissions = this._permissions.slice(0, index).concat(
+ this._permissions.slice(index + 1, this._permissions.length));
+ },
+
_computeLabelOptions(labels) {
const labelOptions = [];
for (const labelName of Object.keys(labels)) {
@@ -184,6 +195,10 @@
},
_handleRemoveReference() {
+ if (this.section.value.added) {
+ this.dispatchEvent(new CustomEvent('added-section-removed',
+ {bubbles: true}));
+ }
this._deleted = true;
this.section.value.deleted = true;
this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
index ea42101..ad401b8 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
@@ -507,6 +507,23 @@
assert.isFalse(element._deleted);
assert.isNotOk(element.section.value.deleted);
});
+
+ test('removing an added permission', () => {
+ element.editing = true;
+ assert.equal(element._permissions.length, 1);
+ element.$$('gr-permission').fire('added-permission-removed');
+ flushAsynchronousOperations();
+ assert.equal(element._permissions.length, 0);
+ });
+
+ test('remove an added section', () => {
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-section-removed', removeStub);
+ element.editing = true;
+ element.section.value.added = true;
+ MockInteractions.tap(element.$.deleteBtn);
+ assert.isTrue(removeStub.called);
+ });
});
});
});
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
index e43f220..3c9cdfb 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -18,11 +18,14 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html">
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
<link rel="import" href="../../../styles/gr-menu-page-styles.html">
<link rel="import" href="../../../styles/gr-page-nav-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html">
+<link rel="import" href="../../shared/gr-icons/gr-icons.html">
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
<link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -42,7 +45,38 @@
<template>
<style include="shared-styles"></style>
<style include="gr-menu-page-styles"></style>
- <style include="gr-page-nav-styles"></style>
+ <style include="gr-page-nav-styles">
+ gr-dropdown-list {
+ --trigger-style: {
+ text-transform: none;
+ }
+ }
+ .breadcrumbText {
+ /* Same as dropdown trigger so chevron spacing is consistent. */
+ padding: 5px 4px;
+ }
+ iron-icon {
+ margin: 0 .2em;
+ }
+ .breadcrumb {
+ align-items: center;
+ display: flex;
+ }
+ .mainHeader {
+ align-items: baseline;
+ border-bottom: 1px solid var(--border-color);
+ display: flex;
+ }
+ .selectText {
+ display: none;
+ }
+ .selectText.show {
+ display: inline-block;
+ }
+ main.breadcrumbs:not(.table) {
+ margin-top: 1em;
+ }
+ </style>
<gr-page-nav class="navStyles">
<ul class="sectionContent">
<template id="adminNav" is="dom-repeat" items="[[_filteredLinks]]">
@@ -74,29 +108,26 @@
</template>
</ul>
</gr-page-nav>
+ <template is="dom-if" if="[[_subsectionLinks.length]]">
+ <section class="mainHeader">
+ <span class="breadcrumb">
+ <span class="breadcrumbText">[[_breadcrumbParentName]]</span>
+ <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+ </span>
+ <gr-dropdown-list
+ lowercase
+ id="pageSelect"
+ value="[[_computeSelectValue(params)]]"
+ items="[[_subsectionLinks]]"
+ on-value-change="_handleSubsectionChange">
+ </gr-dropdown-list>
+ </section>
+ </template>
<template is="dom-if" if="[[_showRepoList]]" restamp="true">
<main class="table">
<gr-repo-list class="table" params="[[params]]"></gr-repo-list>
</main>
</template>
- <template is="dom-if" if="[[_showRepoMain]]" restamp="true">
- <main>
- <gr-repo repo="[[params.repo]]"></gr-repo>
- </main>
- </template>
- <template is="dom-if" if="[[_showGroup]]" restamp="true">
- <main>
- <gr-group
- group-id="[[params.groupId]]"
- on-name-changed="_updateGroupName"></gr-group>
- </main>
- </template>
- <template is="dom-if" if="[[_showGroupMembers]]" restamp="true">
- <main>
- <gr-group-members
- group-id="[[params.groupId]]"></gr-group-members>
- </main>
- </template>
<template is="dom-if" if="[[_showGroupList]]" restamp="true">
<main class="table">
<gr-admin-group-list class="table" params="[[params]]">
@@ -108,35 +139,53 @@
<gr-plugin-list class="table" params="[[params]]"></gr-plugin-list>
</main>
</template>
+ <template is="dom-if" if="[[_showRepoMain]]" restamp="true">
+ <main class="breadcrumbs">
+ <gr-repo repo="[[params.repo]]"></gr-repo>
+ </main>
+ </template>
+ <template is="dom-if" if="[[_showGroup]]" restamp="true">
+ <main class="breadcrumbs">
+ <gr-group
+ group-id="[[params.groupId]]"
+ on-name-changed="_updateGroupName"></gr-group>
+ </main>
+ </template>
+ <template is="dom-if" if="[[_showGroupMembers]]" restamp="true">
+ <main class="breadcrumbs">
+ <gr-group-members
+ group-id="[[params.groupId]]"></gr-group-members>
+ </main>
+ </template>
<template is="dom-if" if="[[_showRepoDetailList]]" restamp="true">
- <main class="table">
+ <main class="table breadcrumbs">
<gr-repo-detail-list
params="[[params]]"
class="table"></gr-repo-detail-list>
</main>
</template>
<template is="dom-if" if="[[_showGroupAuditLog]]" restamp="true">
- <main class="table">
+ <main class="table breadcrumbs">
<gr-group-audit-log
group-id="[[params.groupId]]"
class="table"></gr-group-audit-log>
</main>
</template>
<template is="dom-if" if="[[_showRepoCommands]]" restamp="true">
- <main>
+ <main class="breadcrumbs">
<gr-repo-commands
repo="[[params.repo]]"></gr-repo-commands>
</main>
</template>
<template is="dom-if" if="[[_showRepoAccess]]" restamp="true">
- <main class="table">
+ <main class="breadcrumbs">
<gr-repo-access
path="[[path]]"
repo="[[params.repo]]"></gr-repo-access>
</main>
</template>
<template is="dom-if" if="[[_showRepoDashboards]]" restamp="true">
- <main class="table">
+ <main class="table breadcrumbs">
<gr-repo-dashboards repo="[[params.repo]]"></gr-repo-dashboards>
</main>
</template>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index 66f017a..3d430c2 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -17,35 +17,9 @@
(function() {
'use strict';
- // Note: noBaseUrl: true is set on entries where the URL is not yet supported
- // by router abstraction.
- const ADMIN_LINKS = [{
- name: 'Repositories',
- noBaseUrl: true,
- url: '/admin/repos',
- view: 'gr-repo-list',
- viewableToAll: true,
- children: [],
- }, {
- name: 'Groups',
- section: 'Groups',
- noBaseUrl: true,
- url: '/admin/groups',
- view: 'gr-admin-group-list',
- children: [],
- }, {
- name: 'Plugins',
- capability: 'viewPlugins',
- section: 'Plugins',
- noBaseUrl: true,
- url: '/admin/plugins',
- view: 'gr-plugin-list',
- }];
const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
- const ACCOUNT_CAPABILITIES = ['createProject', 'createGroup', 'viewPlugins'];
-
Polymer({
is: 'gr-admin-view',
@@ -55,6 +29,7 @@
path: String,
adminView: String,
+ _breadcrumbParentName: String,
_repoName: String,
_groupId: {
type: Number,
@@ -66,6 +41,7 @@
type: Boolean,
value: false,
},
+ _subsectionLinks: Array,
_filteredLinks: Array,
_showDownload: {
type: Boolean,
@@ -89,6 +65,7 @@
},
behaviors: [
+ Gerrit.AdminNavBehavior,
Gerrit.BaseUrlBehavior,
Gerrit.URLEncodingBehavior,
],
@@ -108,113 +85,67 @@
];
return Promise.all(promises).then(result => {
this._account = result[0];
- if (!this._account) {
- // Return so that account capabilities don't load with no account.
- return this._filteredLinks = this._filterLinks(link => {
- return link.viewableToAll;
- });
+ let options;
+ if (this._repoName) {
+ options = {repoName: this._repoName};
+ } else if (this._groupId) {
+ options = {
+ groupId: this._groupId,
+ groupName: this._groupName,
+ groupIsInternal: this._groupIsInternal,
+ isAdmin: this._isAdmin,
+ groupOwner: this._groupOwner,
+ };
}
- this._loadAccountCapabilities();
+
+ return this.getAdminLinks(this._account,
+ this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
+ this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI),
+ options)
+ .then(res => {
+ this._filteredLinks = res.links;
+ this._breadcrumbParentName = res.expandedSection ?
+ res.expandedSection.name : '';
+
+ if (!res.expandedSection) {
+ this._subsectionLinks = [];
+ return;
+ }
+ this._subsectionLinks = [res.expandedSection]
+ .concat(res.expandedSection.children).map(section => {
+ return {
+ text: !section.detailType ? 'Home' : section.name,
+ value: section.view + (section.detailType || ''),
+ view: section.view,
+ url: section.url,
+ detailType: section.detailType,
+ parent: this._groupId || this._repoName || '',
+ };
+ });
+ });
});
},
- _filterLinks(filterFn) {
- let links = ADMIN_LINKS.slice(0);
-
- // Append top-level links that are defined by plugins.
- links.push(...this.$.jsAPI.getAdminMenuLinks().map(link => ({
- url: link.url,
- name: link.text,
- children: [],
- noBaseUrl: link.url[0] === '/',
- view: null,
- viewableToAll: true,
- })));
-
- links = links.filter(filterFn);
-
- const filteredLinks = [];
- for (const link of links) {
- const linkCopy = Object.assign({}, link);
- linkCopy.children = linkCopy.children ?
- linkCopy.children.filter(filterFn) : [];
- if (linkCopy.name === 'Repositories' && this._repoName) {
- linkCopy.subsection = {
- name: this._repoName,
- view: Gerrit.Nav.View.REPO,
- url: Gerrit.Nav.getUrlForRepo(this._repoName),
- children: [{
- name: 'Access',
- view: Gerrit.Nav.View.REPO,
- detailType: Gerrit.Nav.RepoDetailView.ACCESS,
- url: Gerrit.Nav.getUrlForRepoAccess(this._repoName),
- },
- {
- name: 'Commands',
- view: Gerrit.Nav.View.REPO,
- detailType: Gerrit.Nav.RepoDetailView.COMMANDS,
- url: Gerrit.Nav.getUrlForRepoCommands(this._repoName),
- },
- {
- name: 'Branches',
- view: Gerrit.Nav.View.REPO,
- detailType: Gerrit.Nav.RepoDetailView.BRANCHES,
- url: Gerrit.Nav.getUrlForRepoBranches(this._repoName),
- },
- {
- name: 'Tags',
- view: Gerrit.Nav.View.REPO,
- detailType: Gerrit.Nav.RepoDetailView.TAGS,
- url: Gerrit.Nav.getUrlForRepoTags(this._repoName),
- },
- {
- name: 'Dashboards',
- view: Gerrit.Nav.View.REPO,
- detailType: Gerrit.Nav.RepoDetailView.DASHBOARDS,
- url: Gerrit.Nav.getUrlForRepoDashboards(this._repoName),
- }],
- };
- }
- if (linkCopy.name === 'Groups' && this._groupId && this._groupName) {
- linkCopy.subsection = {
- name: this._groupName,
- view: Gerrit.Nav.View.GROUP,
- url: Gerrit.Nav.getUrlForGroup(this._groupId),
- children: [],
- };
- if (this._groupIsInternal) {
- linkCopy.subsection.children.push({
- name: 'Members',
- detailType: Gerrit.Nav.GroupDetailView.MEMBERS,
- view: Gerrit.Nav.View.GROUP,
- url: Gerrit.Nav.getUrlForGroupMembers(this._groupId),
- });
- }
- if (this._groupIsInternal && (this._isAdmin || this._groupOwner)) {
- linkCopy.subsection.children.push(
- {
- name: 'Audit Log',
- detailType: Gerrit.Nav.GroupDetailView.LOG,
- view: Gerrit.Nav.View.GROUP,
- url: Gerrit.Nav.getUrlForGroupLog(this._groupId),
- }
- );
- }
- }
-
- filteredLinks.push(linkCopy);
- }
- return filteredLinks;
+ _computeSelectValue(params) {
+ if (!params || !params.view) { return; }
+ return params.view + (params.detail || '');
},
- _loadAccountCapabilities() {
- return this.$.restAPI.getAccountCapabilities(ACCOUNT_CAPABILITIES)
- .then(capabilities => {
- this._filteredLinks = this._filterLinks(link => {
- return !link.capability ||
- capabilities.hasOwnProperty(link.capability);
- });
- });
+ _selectedIsCurrentPage(selected) {
+ return (selected.parent === (this._repoName || this._groupId) &&
+ selected.view === this.params.view &&
+ selected.detailType === this.params.detail);
+ },
+
+ _handleSubsectionChange(e) {
+ const selected = this._subsectionLinks
+ .find(section => section.value === e.detail.value);
+
+ // This is when it gets set initially.
+ if (this._selectedIsCurrentPage(selected)) {
+ return;
+ }
+ Gerrit.Nav.navigateToRelativeUrl(selected.url);
},
_paramsChanged(params) {
@@ -248,16 +179,26 @@
this.set('_showPluginList', isAdminView &&
params.adminView === 'gr-plugin-list');
+ let needsReload = false;
if (params.repo !== this._repoName) {
this._repoName = params.repo || '';
// Reloads the admin menu.
- this.reload();
+ needsReload = true;
}
if (params.groupId !== this._groupId) {
this._groupId = params.groupId || '';
// Reloads the admin menu.
- this.reload();
+ needsReload = true;
}
+ if (this._breadcrumbParentName && !params.groupId && !params.repo) {
+ needsReload = true;
+ }
+ if (!needsReload) { return; }
+ this.reload();
+ },
+
+ _computeSelectedTitle(params) {
+ return this.getSelectedTitle(params.view);
},
// TODO (beckysiegel): Update these functions after router abstraction is
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
index ad1fe46..56079e3 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -37,7 +37,7 @@
let element;
let sandbox;
- setup(() => {
+ setup(done => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
stub('gr-rest-api-interface', {
@@ -45,7 +45,9 @@
return Promise.resolve({});
},
});
- sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
+ const pluginsLoaded = Promise.resolve();
+ sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(pluginsLoaded);
+ pluginsLoaded.then(() => flush(done));
});
teardown(() => {
@@ -79,7 +81,6 @@
name: 'Repositories',
url: '/admin/repos',
view: 'gr-repo-list',
- children: [],
}];
element.params = {
@@ -95,6 +96,9 @@
});
test('_filteredLinks admin', done => {
+ sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+ name: 'test-user',
+ }));
sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => {
return Promise.resolve({
createGroup: true,
@@ -102,35 +106,36 @@
viewPlugins: true,
});
});
- element._loadAccountCapabilities().then(() => {
+ element.reload().then(() => {
assert.equal(element._filteredLinks.length, 3);
// Repos
- assert.equal(element._filteredLinks[0].children.length, 0);
assert.isNotOk(element._filteredLinks[0].subsection);
// Groups
- assert.equal(element._filteredLinks[1].children.length, 0);
+ assert.isNotOk(element._filteredLinks[0].subsection);
// Plugins
- assert.equal(element._filteredLinks[2].children.length, 0);
+ assert.isNotOk(element._filteredLinks[0].subsection);
done();
});
});
test('_filteredLinks non admin authenticated', done => {
+ sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+ name: 'test-user',
+ }));
sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => {
return Promise.resolve({});
});
- element._loadAccountCapabilities().then(() => {
+ element.reload().then(() => {
assert.equal(element._filteredLinks.length, 2);
// Repos
- assert.equal(element._filteredLinks[0].children.length, 0);
assert.isNotOk(element._filteredLinks[0].subsection);
// Groups
- assert.equal(element._filteredLinks[1].children.length, 0);
+ assert.isNotOk(element._filteredLinks[0].subsection);
done();
});
});
@@ -140,7 +145,6 @@
assert.equal(element._filteredLinks.length, 1);
// Repos
- assert.equal(element._filteredLinks[0].children.length, 0);
assert.isNotOk(element._filteredLinks[0].subsection);
done();
});
@@ -156,24 +160,27 @@
assert.deepEqual(element._filteredLinks[1], {
url: '/internal/link/url',
name: 'internal link text',
- children: [],
noBaseUrl: true,
view: null,
viewableToAll: true,
+ target: null,
});
assert.deepEqual(element._filteredLinks[2], {
url: 'http://external/link/url',
name: 'external link text',
- children: [],
noBaseUrl: false,
view: null,
viewableToAll: true,
+ target: '_blank',
});
});
});
test('Repo shows up in nav', done => {
element._repoName = 'Test Repo';
+ sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+ name: 'test-user',
+ }));
sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => {
return Promise.resolve({
createGroup: true,
@@ -181,18 +188,45 @@
viewPlugins: true,
});
});
- element._loadAccountCapabilities().then(() => {
+ element.reload().then(() => {
+ flushAsynchronousOperations();
+ assert.equal(Polymer.dom(element.root)
+ .querySelectorAll('.sectionTitle').length, 3);
+ assert.equal(element.$$('.breadcrumbText').innerText, 'Test Repo');
+ assert.equal(element.$$('#pageSelect').items.length, 6);
+ done();
+ });
+ });
+
+ test('Group shows up in nav', done => {
+ element._groupId = 'a15262';
+ element._groupName = 'my-group';
+ element._groupIsInternal = true;
+ element._isAdmin = true;
+ element._groupOwner = false;
+ sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({
+ name: 'test-user',
+ }));
+ sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => {
+ return Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ });
+ });
+ element.reload().then(() => {
+ flushAsynchronousOperations();
assert.equal(element._filteredLinks.length, 3);
// Repos
- assert.equal(element._filteredLinks[0].children.length, 0);
- assert.equal(element._filteredLinks[0].subsection.name, 'Test Repo');
+ assert.isNotOk(element._filteredLinks[0].subsection);
// Groups
- assert.equal(element._filteredLinks[1].children.length, 0);
+ assert.equal(element._filteredLinks[1].subsection.children.length, 2);
+ assert.equal(element._filteredLinks[1].subsection.name, 'my-group');
// Plugins
- assert.equal(element._filteredLinks[2].children.length, 0);
+ assert.isNotOk(element._filteredLinks[2].subsection);
done();
});
});
@@ -247,6 +281,188 @@
element.$$('gr-group').fire('name-changed', {name: newName});
});
+ test('dropdown displays if there is a subsection', () => {
+ assert.isNotOk(element.$$('.mainHeader'));
+ element._subsectionLinks = [
+ {
+ text: 'Home',
+ value: 'repo',
+ view: 'repo',
+ url: '',
+ parent: 'my-repo',
+ detailType: undefined,
+ },
+ ];
+ flushAsynchronousOperations();
+ assert.isOk(element.$$('.mainHeader'));
+ element._subsectionLinks = undefined;
+ flushAsynchronousOperations();
+ assert.equal(getComputedStyle(element.$$('.mainHeader')).display, 'none');
+ });
+
+ test('Dropdown only triggers navigation on explicit select', done => {
+ element._repoName = 'my-repo';
+ element.params = {
+ repo: 'my-repo',
+ view: Gerrit.Nav.View.REPO,
+ detail: Gerrit.Nav.RepoDetailView.ACCESS,
+ };
+ sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => {
+ return Promise.resolve({
+ createGroup: true,
+ createProject: true,
+ viewPlugins: true,
+ });
+ });
+ sandbox.stub(element.$.restAPI, 'getAccount', () => {
+ return Promise.resolve({_id: 1});
+ });
+ flushAsynchronousOperations();
+ const expectedFilteredLinks = [
+ {
+ name: 'Repositories',
+ noBaseUrl: true,
+ url: '/admin/repos',
+ view: 'gr-repo-list',
+ viewableToAll: true,
+ subsection: {
+ name: 'my-repo',
+ view: 'repo',
+ url: '',
+ children: [
+ {
+ name: 'Access',
+ view: 'repo',
+ detailType: 'access',
+ url: '',
+ },
+ {
+ name: 'Commands',
+ view: 'repo',
+ detailType: 'commands',
+ url: '',
+ },
+ {
+ name: 'Branches',
+ view: 'repo',
+ detailType: 'branches',
+ url: '',
+ },
+ {
+ name: 'Tags',
+ view: 'repo',
+ detailType: 'tags',
+ url: '',
+ },
+ {
+ name: 'Dashboards',
+ view: 'repo',
+ detailType: 'dashboards',
+ url: '',
+ },
+ ],
+ },
+ },
+ {
+ name: 'Groups',
+ section: 'Groups',
+ noBaseUrl: true,
+ url: '/admin/groups',
+ view: 'gr-admin-group-list',
+ },
+ {
+ name: 'Plugins',
+ capability: 'viewPlugins',
+ section: 'Plugins',
+ noBaseUrl: true,
+ url: '/admin/plugins',
+ view: 'gr-plugin-list',
+ },
+ ];
+ const expectedSubsectionLinks = [
+ {
+ text: 'Home',
+ value: 'repo',
+ view: 'repo',
+ url: '',
+ parent: 'my-repo',
+ detailType: undefined,
+ },
+ {
+ text: 'Access',
+ value: 'repoaccess',
+ view: 'repo',
+ url: '',
+ detailType: 'access',
+ parent: 'my-repo',
+ },
+ {
+ text: 'Commands',
+ value: 'repocommands',
+ view: 'repo',
+ url: '',
+ detailType: 'commands',
+ parent: 'my-repo',
+ },
+ {
+ text: 'Branches',
+ value: 'repobranches',
+ view: 'repo',
+ url: '',
+ detailType: 'branches',
+ parent: 'my-repo',
+ },
+ {
+ text: 'Tags',
+ value: 'repotags',
+ view: 'repo',
+ url: '',
+ detailType: 'tags',
+ parent: 'my-repo',
+ },
+ {
+ text: 'Dashboards',
+ value: 'repodashboards',
+ view: 'repo',
+ url: '',
+ detailType: 'dashboards',
+ parent: 'my-repo',
+ },
+ ];
+ sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
+ sandbox.spy(element, '_selectedIsCurrentPage');
+ sandbox.spy(element, '_handleSubsectionChange');
+ element.reload().then(() => {
+ assert.deepEqual(element._filteredLinks, expectedFilteredLinks);
+ assert.deepEqual(element._subsectionLinks, expectedSubsectionLinks);
+ assert.equal(element.$$('#pageSelect').value, 'repoaccess');
+ assert.isTrue(element._selectedIsCurrentPage.calledOnce);
+ // Doesn't trigger navigation from the page select menu.
+ assert.isFalse(Gerrit.Nav.navigateToRelativeUrl.called);
+
+ // When explicitly changed, navigation is called
+ element.$$('#pageSelect').value = 'repo';
+ assert.isTrue(element._selectedIsCurrentPage.calledTwice);
+ assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.calledOnce);
+ done();
+ });
+ });
+
+ test('_selectedIsCurrentPage', () => {
+ element._repoName = 'my-repo';
+ element.params = {view: 'repo', repo: 'my-repo'};
+ const selected = {
+ view: 'repo',
+ detailType: undefined,
+ parent: 'my-repo',
+ };
+ assert.isTrue(element._selectedIsCurrentPage(selected));
+ selected.parent = 'my-second-repo';
+ assert.isFalse(element._selectedIsCurrentPage(selected));
+ selected.detailType = 'detailType';
+ assert.isFalse(element._selectedIsCurrentPage(selected));
+ });
+
suite('_computeSelectedClass', () => {
setup(() => {
sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => {
@@ -343,6 +559,7 @@
}));
sandbox.stub(element.$.restAPI, 'getIsGroupOwner')
.returns(Promise.resolve(true));
+ return element.reload();
});
test('group list', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
index d1fc822..70dfe52 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.html
@@ -40,7 +40,7 @@
gr-autocomplete {
border: none;
--gr-autocomplete: {
- border: 1px solid #d1d2d3;
+ border: 1px solid var(--border-color);
border-radius: 2px;
font-size: var(--font-size-normal);
height: 2em;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
index f2b8855..c1ac650 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.html
@@ -21,6 +21,8 @@
<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/gr-subpage-styles.html">
+<link rel="import" href="../../../styles/gr-table-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
@@ -32,19 +34,9 @@
<dom-module id="gr-group-members">
<template>
<style include="gr-form-styles"></style>
+ <style include="gr-table-styles"></style>
+ <style include="gr-subpage-styles"></style>
<style include="shared-styles">
- main {
- margin: 2em 1em;
- }
- .loading {
- display: none;
- }
- #loading.loading {
- display: block;
- }
- #loading:not(.loading) {
- display: none;
- }
.input {
width: 15em;
}
@@ -57,14 +49,14 @@
}
}
a {
- color: var(--default-text-color);
+ color: var(--primary-text-color);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
th {
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid var(--border-color);
font-family: var(--font-family-bold);
text-align: left;
}
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
index e9b7aa3..d21247a 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.html
@@ -19,6 +19,7 @@
<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/gr-subpage-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html">
@@ -28,23 +29,12 @@
<dom-module id="gr-group">
<template>
- <style include="shared-styles">
- main {
- margin: 2em 1em;
- }
+ <style include="shared-styles"></style>
+ <style include="gr-subpage-styles">
h3.edited:after {
color: #444;
content: ' *';
}
- .loading {
- display: none;
- }
- #loading.loading {
- display: block;
- }
- #loading:not(.loading) {
- display: none;
- }
.inputUpdateBtn {
margin-top: .3em;
}
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
index cc0ca51..22f461b 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
@@ -40,12 +40,12 @@
margin: .3em .7em;
}
.rules {
- background: #fafafa;
- border: 1px solid #d1d2d3;
+ background: var(--table-header-background-color);
+ border: 1px solid var(--border-color);
border-bottom: 0;
}
.editing .rules {
- border-bottom: 1px solid #d1d2d3;
+ border-bottom: 1px solid var(--border-color);
}
.title {
margin-bottom: .3em;
@@ -72,7 +72,7 @@
}
.deleted #deletedContainer {
align-items: baseline;
- border: 1px solid #d1d2d3;
+ border: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
padding: .7em;
@@ -115,7 +115,8 @@
group-name="[[_computeGroupName(groups, rule.id)]]"
permission="[[permission.id]]"
rule="{{rule}}"
- section="[[section]]"></gr-rule-editor>
+ section="[[section]]"
+ on-added-rule-removed="_handleAddedRuleRemoved"></gr-rule-editor>
</template>
<div id="addRule">
<gr-autocomplete
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
index 01caf1d..31d371d 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -25,6 +25,11 @@
* @event access-modified
*/
+ /**
+ * Fired when a permission that was previously added was removed.
+ * @event added-permission-removed
+ */
+
Polymer({
is: 'gr-permission',
@@ -117,6 +122,12 @@
}
},
+ _handleAddedRuleRemoved(e) {
+ const index = e.model.index;
+ this._rules = this._rules.slice(0, index)
+ .concat(this._rules.slice(index + 1, this._rules.length));
+ },
+
_handleValueChange() {
this.permission.value.modified = true;
// Allows overall access page to know a change has been made.
@@ -124,6 +135,10 @@
},
_handleRemovePermission() {
+ if (this.permission.value.added) {
+ this.dispatchEvent(new CustomEvent('added-permission-removed',
+ {bubbles: true}));
+ }
this._deleted = true;
this.permission.value.deleted = true;
this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
index 6799d10..e29c4a2 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -327,11 +327,36 @@
assert.equal(Object.keys(element.permission.value.rules).length, 2);
});
+ test('removing an added rule', () => {
+ element.name = 'Priority';
+ element.section = 'refs/*';
+ element.groups = {};
+ element.$.groupAutocomplete.text = 'new group name';
+ assert.equal(element._rules.length, 2);
+ element.$$('gr-rule-editor').fire('added-rule-removed');
+ flushAsynchronousOperations();
+ assert.equal(element._rules.length, 1);
+ });
+
+ test('removing an added permission', () => {
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-permission-removed', removeStub);
+ element.editing = true;
+ element.name = 'Priority';
+ element.section = 'refs/*';
+ element.permission.value.added = true;
+ MockInteractions.tap(element.$.removeBtn);
+ assert.isTrue(removeStub.called);
+ });
+
test('removing the permission', () => {
element.editing = true;
element.name = 'Priority';
element.section = 'refs/*';
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-permission-removed', removeStub);
+
assert.isFalse(element.$.permission.classList.contains('deleted'));
assert.isFalse(element._deleted);
MockInteractions.tap(element.$.removeBtn);
@@ -340,6 +365,7 @@
MockInteractions.tap(element.$.undoRemoveBtn);
assert.isFalse(element.$.permission.classList.contains('deleted'));
assert.isFalse(element._deleted);
+ assert.isFalse(removeStub.called);
});
test('modify a permission', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
index 47f47ec..0ceac7c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.html
@@ -22,6 +22,7 @@
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
<link rel="import" href="../../../styles/gr-menu-page-styles.html">
+<link rel="import" href="../../../styles/gr-subpage-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -31,14 +32,13 @@
<dom-module id="gr-repo-access">
<template>
- <style include="shared-styles">
+ <style include="shared-styles"></style>
+ <style include="gr-subpage-styles">
gr-button,
#inheritsFrom,
#editInheritFromInput,
.editing #inheritFromName,
- .weblinks,
- #loadingText,
- .loading {
+ .weblinks{
display: none;
}
#inheritsFrom.show {
@@ -50,7 +50,6 @@
margin-right: .2em;
}
.weblinks.show,
- #loadingText.loading,
.referenceContainer {
display: block;
}
@@ -69,7 +68,7 @@
</style>
<style include="gr-menu-page-styles"></style>
<main class$="[[_computeMainClass(_isAdmin, _canUpload, _editing)]]">
- <div id="loadingText" class$="[[_computeLoadingClass(_loading)]]">
+ <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
Loading...
</div>
<div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
@@ -104,13 +103,16 @@
<template
is="dom-repeat"
items="{{_sections}}"
+ initial-count="5"
+ target-framerate="60"
as="section">
<gr-access-section
capabilities="[[_capabilities]]"
section="{{section}}"
labels="[[_labels]]"
editing="[[_editing]]"
- groups="[[_groups]]"></gr-access-section>
+ groups="[[_groups]]"
+ on-added-section-removed="_handleAddedSectionRemoved"></gr-access-section>
</template>
<div class="referenceContainer">
<gr-button id="addReferenceBtn"
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
index 853e316..f307090 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -243,6 +243,12 @@
return inheritsFrom ? 'show' : '';
},
+ _handleAddedSectionRemoved(e) {
+ const index = e.model.index;
+ this._sections = this._sections.slice(0, index)
+ .concat(this._sections.slice(index + 1, this._sections.length));
+ },
+
_handleEditingChanged(editing, editingOld) {
// Ignore when editing gets set initially.
if (!editingOld || editing) { return; }
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
index 66fece3..2f0b1b8 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
@@ -301,6 +301,14 @@
flushAsynchronousOperations();
});
+ test('removing an added section', () => {
+ element.editing = true;
+ assert.equal(element._sections.length, 1);
+ element.$$('gr-access-section').fire('added-section-removed');
+ flushAsynchronousOperations();
+ assert.equal(element._sections.length, 0);
+ });
+
test('button visibility for non admin', () => {
assert.equal(getComputedStyle(element.$.saveBtn).display, 'none');
assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html
index b686237..510f654 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.html
@@ -19,6 +19,7 @@
<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="../../../bower_components/iron-input/iron-input.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/gr-subpage-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
@@ -30,20 +31,8 @@
<dom-module id="gr-repo-commands">
<template>
- <style include="shared-styles">
- main {
- margin: 2em 1em;
- }
- .loading {
- display: none;
- }
- #loading.loading {
- display: block;
- }
- #loading:not(.loading) {
- display: none;
- }
- </style>
+ <style include="shared-styles"></style>
+ <style include="gr-subpage-styles"></style>
<style include="gr-form-styles"></style>
<main class="gr-form-styles read-only">
<h1 id="Title">Repository Commands</h1>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html
index fdbc239..f3892ea 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.html
@@ -16,6 +16,7 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-repo-dashboards">
@@ -52,7 +53,7 @@
</tr>
<template is="dom-repeat" items="[[item.dashboards]]">
<tr class="table">
- <td class="name"><a href$="[[item.url]]">[[item.path]]</a></td>
+ <td class="name"><a href$="[[_getUrl(item.url)]]">[[item.path]]</a></td>
<td class="title">[[item.title]]</td>
<td class="desc">[[item.description]]</td>
<td class="inherited">[[_computeInheritedFrom(item.project, item.defining_project)]]</td>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
index a4c2c03..a852533 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
@@ -68,6 +68,12 @@
});
},
+ _getUrl(url) {
+ if (!url) { return ''; }
+
+ return Gerrit.Nav.navigateToRelativeUrl(url);
+ },
+
_computeLoadingClass(loading) {
return loading ? 'loading' : '';
},
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
index bd4019e..ab57ba5 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
@@ -243,6 +243,17 @@
});
});
+ suite('test url', () => {
+ test('_getUrl', () => {
+ sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl',
+ () => '/r/dashboard/test');
+
+ assert.equal(element._getUrl('/dashboard/test'), '/r/dashboard/test');
+
+ assert.equal(element._getUrl(undefined), '');
+ });
+ });
+
suite('404', () => {
test('fires page-error', done => {
const response = {status: 404};
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
index 2c0a737..5560972 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
@@ -63,7 +63,10 @@
type: Boolean,
value: true,
},
- _filter: String,
+ _filter: {
+ type: String,
+ value: '',
+ },
},
behaviors: [
@@ -115,7 +118,8 @@
this._repos = [];
return this.$.restAPI.getRepos(filter, reposPerPage, offset)
.then(repos => {
- if (!repos) { return; }
+ // Late response.
+ if (filter !== this._filter || !repos) { return; }
this._repos = Object.keys(repos)
.map(key => {
const repo = repos[key];
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
index 731437fa..4bc023f 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
@@ -56,6 +56,7 @@
setup(() => {
sandbox = sinon.sandbox.create();
+ sandbox.stub(page, 'show');
element = fixture('basic');
counter = 0;
});
@@ -118,6 +119,11 @@
});
suite('filter', () => {
+ setup(() => {
+ repos = _.times(25, repoGenerator);
+ reposFiltered = _.times(1, repoGenerator);
+ });
+
test('_paramsChanged', done => {
sandbox.stub(element.$.restAPI, 'getRepos', () => {
return Promise.resolve(repos);
@@ -132,6 +138,19 @@
done();
});
});
+
+ test('latest repos requested are always set', done => {
+ const repoStub = sandbox.stub(element.$.restAPI, 'getRepos');
+ repoStub.withArgs('test').returns(Promise.resolve(repos));
+ repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered));
+ element._filter = 'test';
+
+ // Repos are not set because the element._filter differs.
+ element._getRepos('filter', 25, 0).then(() => {
+ assert.deepEqual(element._repos, []);
+ done();
+ });
+ });
});
suite('loading', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
index 3eb9d62..85dcdbe 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.html
@@ -24,14 +24,14 @@
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../shared/gr-select/gr-select.html">
<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../../styles/gr-subpage-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
+
<dom-module id="gr-repo">
<template>
- <style include="shared-styles">
- main {
- margin: 2em 1em;
- }
+ <style="shared-styles"></style>
+ <style include="gr-subpage-styles">
h2.edited:after {
color: #444;
content: ' *';
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
index 2d05a3b..febd446 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
@@ -29,7 +29,7 @@
<template>
<style include="shared-styles">
:host {
- border-bottom: 1px solid #d1d2d3;
+ border-bottom: 1px solid var(--border-color);
padding: .7em;
display: block;
}
@@ -68,7 +68,7 @@
display: block;
}
.groupPath {
- color: #666;
+ color: var(--deemphasized-text-color);
}
</style>
<style include="gr-form-styles"></style>
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index 0a719be..4af4952 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -23,6 +23,11 @@
* @event access-modified
*/
+ /**
+ * Fired when a rule that was previously added was removed.
+ * @event added-rule-removed
+ */
+
const PRIORITY_OPTIONS = [
'BATCH',
'INTERACTIVE',
@@ -36,11 +41,11 @@
const FORCE_PUSH_OPTIONS = [
{
- name: 'No Force Push',
+ name: 'Block all pushes, block force push only',
value: false,
},
{
- name: 'Force Push',
+ name: 'Allow fast-forward only push, allow all pushes',
value: true,
},
];
@@ -188,6 +193,10 @@
},
_handleRemoveRule() {
+ if (this.rule.value.added) {
+ this.dispatchEvent(new CustomEvent('added-rule-removed',
+ {bubbles: true}));
+ }
this._deleted = true;
this.rule.value.deleted = true;
this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
index 864bf16..5b6f947 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
@@ -52,11 +52,11 @@
() => {
const FORCE_PUSH_OPTIONS = [
{
- name: 'No Force Push',
+ name: 'Block all pushes, block force push only',
value: false,
},
{
- name: 'Force Push',
+ name: 'Allow fast-forward only push, allow all pushes',
value: true,
},
];
@@ -304,6 +304,7 @@
element.section = 'refs/*';
element._setupValues(element.rule);
flushAsynchronousOperations();
+ element.rule.value.added = true;
});
test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -313,9 +314,9 @@
const expectedRuleValue = {
action: 'ALLOW',
force: false,
+ added: true,
};
assert.deepEqual(element.rule.value, expectedRuleValue);
- assert.deepEqual(element._originalRuleValues, expectedRuleValue);
test('values are set correctly', () => {
assert.equal(element.$.action.bindValue, expectedRuleValue.action);
assert.equal(element.$.force.bindValue, expectedRuleValue.action);
@@ -331,6 +332,15 @@
// The original value should now differ from the rule values.
assert.notDeepEqual(element._originalRuleValues, element.rule.value);
});
+
+ test('remove value', () => {
+ element.editing = true;
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-rule-removed', removeStub);
+ MockInteractions.tap(element.$.removeBtn);
+ flushAsynchronousOperations();
+ assert.isTrue(removeStub.called);
+ });
});
suite('already existing rule with labels', () => {
@@ -374,10 +384,13 @@
});
test('modify value', () => {
+ const removeStub = sandbox.stub();
+ element.addEventListener('added-rule-removed', removeStub);
assert.isNotOk(element.rule.value.modified);
Polymer.dom(element.root).querySelector('#labelMin').bindValue = 1;
flushAsynchronousOperations();
assert.isTrue(element.rule.value.modified);
+ assert.isFalse(removeStub.called);
// The original value should now differ from the rule values.
assert.notDeepEqual(element._originalRuleValues, element.rule.value);
@@ -402,6 +415,7 @@
element.section = 'refs/*';
element._setupValues(element.rule);
flushAsynchronousOperations();
+ element.rule.value.added = true;
});
test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -414,9 +428,9 @@
max: element.label.values[element.label.values.length - 1].value,
min: element.label.values[0].value,
action: 'ALLOW',
+ added: true,
};
assert.deepEqual(element.rule.value, expectedRuleValue);
- assert.deepEqual(element._originalRuleValues, expectedRuleValue);
test('values are set correctly', () => {
assert.equal(
element.$.action.bindValue,
@@ -492,6 +506,7 @@
element.section = 'refs/*';
element._setupValues(element.rule);
flushAsynchronousOperations();
+ element.rule.value.added = true;
});
test('_ruleValues and _originalRuleValues are set correctly', () => {
@@ -501,9 +516,9 @@
const expectedRuleValue = {
action: 'ALLOW',
force: false,
+ added: true,
};
assert.deepEqual(element.rule.value, expectedRuleValue);
- assert.deepEqual(element._originalRuleValues, expectedRuleValue);
test('values are set correctly', () => {
assert.equal(element.$.action.bindValue, expectedRuleValue.action);
assert.equal(element.$.force.bindValue, expectedRuleValue.action);
@@ -561,44 +576,5 @@
assert.notDeepEqual(element._originalRuleValues, element.rule.value);
});
});
-
- suite('new edit rule', () => {
- setup(() => {
- element.group = 'Group Name';
- element.permission = 'editTopicName';
- element.rule = {
- id: '123',
- };
- element.section = 'refs/*';
- element._setupValues(element.rule);
- flushAsynchronousOperations();
- });
-
- test('_ruleValues and _originalRuleValues are set correctly', () => {
- // Since the element does not already have default values, they should
- // be set. The original values should be set to those too.
- assert.isNotOk(element.rule.value.modified);
- const expectedRuleValue = {
- action: 'ALLOW',
- force: false,
- };
- assert.deepEqual(element.rule.value, expectedRuleValue);
- assert.deepEqual(element._originalRuleValues, expectedRuleValue);
- test('values are set correctly', () => {
- assert.equal(element.$.action.bindValue, expectedRuleValue.action);
- assert.equal(element.$.force.bindValue, expectedRuleValue.action);
- });
- });
-
- test('modify value', () => {
- assert.isNotOk(element.rule.value.modified);
- element.$.force.bindValue = true;
- flushAsynchronousOperations();
- assert.isTrue(element.rule.value.modified);
-
- // The original value should now differ from the rule values.
- assert.notDeepEqual(element._originalRuleValues, element.rule.value);
- });
- });
});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index 6005b92..1ee0b6e 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -36,16 +36,11 @@
:host {
display: table-row;
}
- :host:focus {
+ :host(:focus) {
outline: none;
}
:host(:hover) {
- background-color: #eeeeee;
- }
- :host([selected]) {
- /* Double the value used in the file list due to the parent table having
- `border-collapse: collapse;` */
- border-left: .7rem solid var(--color-link);
+ background-color: var(--hover-background-color);
}
:host([needs-review]) {
font-family: var(--font-family-bold);
@@ -91,7 +86,7 @@
padding: .4rem .6rem;
}
a {
- color: var(--default-text-color);
+ color: var(--primary-text-color);
cursor: pointer;
display: inline-block;
text-decoration: none;
@@ -99,12 +94,6 @@
a:hover {
text-decoration: underline;
}
- .positionIndicator {
- visibility: hidden;
- }
- :host([selected]) .positionIndicator {
- visibility: visible;
- }
.u-monospace {
font-family: var(--monospace-font-family);
}
@@ -123,19 +112,16 @@
}
.comma,
.placeholder {
- color: rgba(0, 0, 0, .54);
+ color: var(--deemphasized-text-color);
}
@media only screen and (max-width: 50em) {
:host {
display: flex;
}
- :host([selected]) {
- border-left: none;
- }
}
</style>
<style include="gr-change-list-styles"></style>
- <td class="cell keyboard"></td>
+ <td class="cell leftPadding"></td>
<td class="cell star" hidden$="[[!showStar]]" hidden>
<gr-change-star change="{{change}}"></gr-change-star>
</td>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index a9c3c6f..0ce754d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -22,6 +22,7 @@
<link rel="import" href="../../shared/gr-icons/gr-icons.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-change-list/gr-change-list.html">
+<link rel="import" href="../gr-repo-header/gr-repo-header.html">
<link rel="import" href="../gr-user-header/gr-user-header.html">
<link rel="import" href="../../../styles/shared-styles.html">
@@ -33,14 +34,15 @@
display: block;
}
.loading {
- color: #666;
+ color: var(--deemphasized-text-color);
padding: 1em var(--default-horizontal-margin);
}
gr-change-list {
width: 100%;
}
- gr-user-header {
- border-bottom: 1px solid #ddd;
+ gr-user-header,
+ gr-repo-header {
+ border-bottom: 1px solid var(--border-color);
}
nav {
align-items: center;
@@ -71,11 +73,14 @@
</style>
<div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div>
<div hidden$="[[_loading]]" hidden>
+ <gr-repo-header
+ repo="[[_repo]]"
+ class$="[[_computeHeaderClass(_repo)]]"></gr-repo-header>
<gr-user-header
user-id="[[_userId]]"
show-dashboard-link
logged-in="[[_loggedIn]]"
- class$="[[_computeUserHeaderClass(_userId)]]"></gr-user-header>
+ class$="[[_computeHeaderClass(_userId)]]"></gr-user-header>
<gr-change-list
account="[[account]]"
changes="{{_changes}}"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index 1728bc1..2a05a2f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -24,6 +24,9 @@
const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/;
+ const REPO_QUERY_PATTERN =
+ /^project:\s?("[^"]+"|[^ ]+)(\sstatus\s?:(open|"open"))?$/;
+
const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
Polymer({
@@ -114,6 +117,12 @@
type: String,
value: null,
},
+
+ /** @type {?String} */
+ _repo: {
+ type: String,
+ value: null,
+ },
},
listeners: {
@@ -138,7 +147,9 @@
this.set('viewState.offset', this._offset);
}
- this.fire('title-change', {title: this._query});
+ // NOTE: This method may be called before attachment. Fire title-change
+ // in an async so that attachment to the DOM can take place first.
+ this.async(() => this.fire('title-change', {title: this._query}));
this._getPreferences().then(prefs => {
this._changesPerPage = prefs.changes_per_page;
@@ -227,16 +238,22 @@
},
_changesChanged(changes) {
- if (!changes || !changes.length ||
- !USER_QUERY_PATTERN.test(this._query)) {
- this._userId = null;
+ this._userId = null;
+ this._repo = null;
+ if (!changes || !changes.length) {
return;
}
- this._userId = changes[0].owner.email;
+ if (USER_QUERY_PATTERN.test(this._query) && changes[0].owner.email) {
+ this._userId = changes[0].owner.email;
+ return;
+ }
+ if (REPO_QUERY_PATTERN.test(this._query)) {
+ this._repo = changes[0].project;
+ }
},
- _computeUserHeaderClass(userId) {
- return userId ? '' : 'hide';
+ _computeHeaderClass(id) {
+ return id ? '' : 'hide';
},
_computePage(offset, changesPerPage) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
index 2091c7d..3911364 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -156,6 +156,42 @@
});
});
+ test('_userId query without email', done => {
+ assert.isNull(element._userId);
+ element._query = 'owner: foo@bar';
+ element._changes = [{owner: {}}];
+ flush(() => {
+ assert.isNull(element._userId);
+ done();
+ });
+ });
+
+ test('_repo query', done => {
+ assert.isNull(element._repo);
+ element._query = 'project: test-repo';
+ element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
+ flush(() => {
+ assert.equal(element._repo, 'test-repo');
+ element._query = 'foo bar baz';
+ element._changes = [{owner: {email: 'foo@bar'}}];
+ assert.isNull(element._repo);
+ done();
+ });
+ });
+
+ test('_repo query with open status', done => {
+ assert.isNull(element._repo);
+ element._query = 'project:test-repo status:open';
+ element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}];
+ flush(() => {
+ assert.equal(element._repo, 'test-repo');
+ element._query = 'foo bar baz';
+ element._changes = [{owner: {email: 'foo@bar'}}];
+ assert.isNull(element._repo);
+ done();
+ });
+ });
+
suite('query based navigation', () => {
setup(() => {
sandbox.stub(Gerrit.Nav, 'getUrlForChange', () => '/r/c/1');
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
index d917423..3509f35 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -38,7 +38,7 @@
</style>
<table id="changeList">
<tr class="topHeader">
- <th class="keyboard"></th>
+ <th class="leftPadding"></th>
<th class="star" hidden$="[[!showStar]]" hidden></th>
<th class="number" hidden$="[[!showNumber]]" hidden>#</th>
<template is="dom-repeat" items="[[changeTableColumns]]" as="item">
@@ -57,7 +57,7 @@
index-as="sectionIndex">
<template is="dom-if" if="[[changeSection.sectionName]]">
<tr class="groupHeader">
- <td class="keyboard"></td>
+ <td class="leftPadding"></td>
<td class="star" hidden$="[[!showStar]]" hidden></td>
<td class="cell"
colspan$="[[_computeColspan(changeTableColumns, labelNames)]]">
@@ -69,7 +69,7 @@
</template>
<template is="dom-if" if="[[!changeSection.results.length]]">
<tr class="noChanges">
- <td class="keyboard"></td>
+ <td class="leftPadding"></td>
<td class="star" hidden$="[[!showStar]]" hidden></td>
<td class="cell"
colspan$="[[_computeColspan(changeTableColumns, labelNames)]]">
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
index d97e7f4..1935962 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -15,10 +15,11 @@
limitations under the License.
-->
-<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../change-list/gr-change-list/gr-change-list.html">
+<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-user-header/gr-user-header.html">
@@ -30,7 +31,7 @@
display: block;
}
.loading {
- color: #666;
+ color: var(--deemphasized-text-color);
padding: 1em var(--default-horizontal-margin);
}
gr-change-list {
@@ -40,7 +41,7 @@
display: none;
}
gr-user-header {
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color);
}
@media only screen and (max-width: 50em) {
.loading {
@@ -62,6 +63,7 @@
sections="[[_results]]"></gr-change-list>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-reporting id="reporting"></gr-reporting>
</template>
<script src="gr-dashboard-view.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index b6e06d8..f86c98c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -230,6 +230,8 @@
};
});
});
+ }).then(() => {
+ this.$.reporting.dashboardDisplayed();
}).catch(err => {
this._loading = false;
console.warn(err);
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
index a3aa4f2..a1da018 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -55,7 +55,7 @@
});
const paramsChanged = element._paramsChanged.bind(element);
sandbox.stub(element, '_paramsChanged', params => {
- paramsChanged(params).then(resolver());
+ paramsChanged(params).then(() => resolver());
});
});
@@ -262,5 +262,17 @@
dashboard: 'dashboard',
};
});
+
+ test('params change triggers dashboardDisplayed()', () => {
+ sandbox.stub(element.$.reporting, 'dashboardDisplayed');
+ element.params = {
+ view: Gerrit.Nav.View.DASHBOARD,
+ project: 'project',
+ dashboard: 'dashboard',
+ };
+ return paramsChangedPromise.then(() => {
+ assert.isTrue(element.$.reporting.dashboardDisplayed.calledOnce);
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html
new file mode 100644
index 0000000..2328725
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.html
@@ -0,0 +1,41 @@
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/dashboard-header-styles.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<dom-module id="gr-repo-header">
+ <template>
+ <style include="shared-styles"></style>
+ <style include="dashboard-header-styles"></style>
+ <div class="info">
+ <h1 class$="name">
+ [[repo]]
+ <hr/>
+ </h1>
+ <div>
+ <span>Detail:</span> <a href$="[[_repoUrl]]">Repo settings</a>
+ </div>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </template>
+ <script src="gr-repo-header.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
new file mode 100644
index 0000000..cd6eb77
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
@@ -0,0 +1,40 @@
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-repo-header',
+ properties: {
+ /** @type {?String} */
+ repo: {
+ type: String,
+ observer: '_repoChanged',
+ },
+ /** @type {String|null} */
+ _repoUrl: String,
+ },
+
+ _repoChanged(repoName) {
+ if (!repoName) {
+ this._repoUrl = null;
+ return;
+ }
+ this._repoUrl = Gerrit.Nav.getUrlForRepo(repoName);
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
new file mode 100644
index 0000000..a561e09
--- /dev/null
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-repo-header</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-repo-header.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-repo-header></gr-repo-header>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-repo-header tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => { sandbox.restore(); });
+
+ test('loads and clears account info', done => {
+ sandbox.stub(element.$.restAPI, 'getAccountDetails')
+ .returns(Promise.resolve({
+ name: 'foo',
+ email: 'bar',
+ registered_on: '2015-03-12 18:32:08.000000000',
+ }));
+ sandbox.stub(element.$.restAPI, 'getAccountStatus')
+ .returns(Promise.resolve('baz'));
+
+ element.userId = 'foo.bar@baz';
+ flush(() => {
+ assert.isOk(element._accountDetails);
+ assert.isOk(element._status);
+
+ element.userId = null;
+ flush(() => {
+ flushAsynchronousOperations();
+ assert.isNull(element._accountDetails);
+ assert.isNull(element._status);
+
+ done();
+ });
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
index 1c19ab2..89e2b7d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
@@ -20,35 +20,13 @@
<link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../../styles/dashboard-header-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<dom-module id="gr-user-header">
<template>
- <style include="shared-styles">
- :host {
- display: block;
- height: 9em;
- width: 100%;
- }
- gr-avatar {
- display: inline-block;
- height: 7em;
- left: 1em;
- margin: 1em;
- top: 1em;
- width: 7em;
- }
- .info {
- display: inline-block;
- padding: 1em;
- vertical-align: top;
- }
- .info > div > span {
- display: inline-block;
- font-weight: bold;
- text-align: right;
- width: 4em;
- }
+ <style include="shared-styles"></style>
+ <style include="dashboard-header-styles">
.name {
display: inline-block;
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index d54bbc3..62e9033 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -55,7 +55,7 @@
}
#actionLoadingMessage {
align-items: center;
- color: #777;
+ color: var(--deemphasized-text-color);
}
#confirmSubmitDialog .changeSubject {
margin: 1em;
@@ -95,7 +95,9 @@
is="dom-repeat"
items="[[_topLevelPrimaryActions]]"
as="action">
- <gr-button title$="[[action.title]]"
+ <gr-button
+ title$="[[action.title]]"
+ has-tooltip="[[_computeHasTooltip(action.title)]]"
primary$="[[action.__primary]]"
data-action-key$="[[action.__key]]"
data-action-type$="[[action.__type]]"
@@ -110,7 +112,9 @@
is="dom-repeat"
items="[[_topLevelSecondaryActions]]"
as="action">
- <gr-button title$="[[action.title]]"
+ <gr-button
+ title$="[[action.title]]"
+ has-tooltip="[[_computeHasTooltip(action.title)]]"
primary$="[[action.__primary]]"
data-action-key$="[[action.__key]]"
data-action-type$="[[action.__type]]"
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 e3220a8..3f967c8 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
@@ -385,7 +385,7 @@
ready() {
this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this);
- this._loading = false;
+ this._handleLoadingComplete();
},
reload() {
@@ -398,7 +398,7 @@
if (!revisionActions) { return; }
this.revisionActions = revisionActions;
- this._loading = false;
+ this._handleLoadingComplete();
}).catch(err => {
this.fire('show-alert', {message: ERR_REVISION_ACTIONS});
this._loading = false;
@@ -406,6 +406,10 @@
});
},
+ _handleLoadingComplete() {
+ Gerrit.awaitPluginsLoaded().then(() => this._loading = false);
+ },
+
_changeChanged() {
this.reload();
},
@@ -1332,6 +1336,7 @@
name: action.label,
id: `${key}-${action.__type}`,
action,
+ tooltip: action.title,
};
});
},
@@ -1377,5 +1382,9 @@
_handleStopEditTap() {
this.dispatchEvent(new CustomEvent('stop-edit-tap', {bubbles: false}));
},
+
+ _computeHasTooltip(title) {
+ return !!title;
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
index 9d51339..ff9e547 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -29,7 +29,7 @@
<test-fixture id="element">
<template>
- <gr-change-metadata></gr-change-metadata>
+ <gr-change-metadata mutable="true"></gr-change-metadata>
</template>
</test-fixture>
@@ -46,37 +46,55 @@
const sectionSelectors = [
'section.assignee',
- 'section.labelStatus',
'section.strategy',
'section.topic',
];
+ const labels = {
+ CI: {
+ all: [
+ {value: 1, name: 'user 2', _account_id: 1},
+ {value: 2, name: 'user '},
+ ],
+ values: {
+ ' 0': 'Don\'t submit as-is',
+ '+1': 'No score',
+ '+2': 'Looks good to me',
+ },
+ },
+ };
+
const getStyle = function(selector, name) {
return window.getComputedStyle(
Polymer.dom(element.root).querySelector(selector))[name];
};
+ function createElement() {
+ const element = fixture('element');
+ element.change = {labels, status: 'NEW'};
+ element.revision = {};
+ return element;
+ }
+
setup(() => {
sandbox = sinon.sandbox.create();
+ stub('gr-rest-api-interface', {
+ getConfig() { return Promise.resolve({}); },
+ getLoggedIn() { return Promise.resolve(false); },
+ deleteVote() { return Promise.resolve({ok: true}); },
+ });
stub('gr-change-metadata', {
- _computeShowLabelStatus() { return true; },
_computeShowReviewersByState() { return true; },
- ready() {
- this.change = {labels: [], status: 'NEW'};
- this.serverConfig = {};
- },
});
});
teardown(() => {
- Gerrit._pluginsPending = -1;
- Gerrit._allPluginsPromise = undefined;
sandbox.restore();
});
suite('by default', () => {
setup(done => {
- element = fixture('element');
+ element = createElement();
flush(done);
});
@@ -89,6 +107,7 @@
suite('with plugin style', () => {
setup(done => {
+ Gerrit._resetPlugins();
const pluginHost = fixture('plugin-host');
pluginHost.config = {
plugin: {
@@ -99,7 +118,7 @@
],
},
};
- element = fixture('element');
+ element = createElement();
const importSpy = sandbox.spy(element.$.externalStyle, '_import');
Gerrit.awaitPluginsLoaded().then(() => {
Promise.all(importSpy.returnValues).then(() => {
@@ -114,5 +133,38 @@
});
}
});
+
+ suite('label updates', () => {
+ let plugin;
+ let labelChangeStub;
+
+ setup(done => {
+ Gerrit.install(p => plugin = p, '0.1',
+ new URL('test/plugin.html?' + Math.random(),
+ window.location.href).toString());
+ sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+ Gerrit._setPluginsPending([]);
+ element = createElement();
+ sandbox.stub(element, '_computeCanDeleteVote').returns(true);
+
+ labelChangeStub = sandbox.stub();
+ plugin.changeMetadata().onLabelsChanged(labelChangeStub);
+ flush(done);
+ });
+
+ test('labels changed callback', done => {
+ assert.equal(labelChangeStub.callCount, 1);
+ assert.isTrue(labelChangeStub.calledWithExactly(labels));
+ assert.equal(labelChangeStub.args[0][0]['CI'].all.length, 2);
+ MockInteractions.tap(Polymer.dom(element.root).querySelector(
+ 'gr-account-chip').$.remove);
+ // Wait for fake rest API response.
+ flush(() => {
+ assert.equal(labelChangeStub.callCount, 2);
+ assert.equal(labelChangeStub.args[1][0]['CI'].all.length, 1);
+ done();
+ });
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index eae9d2e..e5728d8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -28,9 +28,11 @@
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
<link rel="import" href="../../shared/gr-label/gr-label.html">
+<link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html">
<link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html">
<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-change-requirements/gr-change-requirements.html">
<link rel="import" href="../gr-commit-info/gr-commit-info.html">
<link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html">
@@ -49,7 +51,7 @@
display: block;
}
.title {
- color: #666;
+ color: var(--deemphasized-text-color);
font-family: var(--font-family-bold);
max-width: 20em;
word-break: break-word;
@@ -93,26 +95,13 @@
.negative {
background-color: var(--vote-color-negative);
}
- .labelStatus .value {
- max-width: 9em;
- }
- .labelStatus li {
- list-style-type: disc;
- }
.webLink {
display: block;
}
- #missingLabels {
- padding-left: 1.5em;
- }
-
/* CSS Mixins should be applied last. */
section.assignee {
@apply --change-metadata-assignee;
}
- section.labelStatus {
- @apply --change-metadata-label-status;
- }
section.strategy {
@apply --change-metadata-strategy;
}
@@ -233,13 +222,17 @@
<section>
<span class="title">Project</span>
<span class="value">
- <a href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a>
+ <a href$="[[_computeProjectURL(change.project)]]">
+ <gr-limited-text limit="40" text="[[change.project]]"></gr-limited-text>
+ </a>
</span>
</section>
<section>
<span class="title">Branch</span>
<span class="value">
- <a href$="[[_computeBranchURL(change.project, change.branch)]]">[[change.branch]]</a>
+ <a href$="[[_computeBranchURL(change.project, change.branch)]]">
+ <gr-limited-text limit="40" text="[[change.branch]]"></gr-limited-text>
+ </a>
</span>
</section>
<section>
@@ -316,12 +309,12 @@
</section>
</template>
<template is="dom-repeat"
- items="[[_computeLabelNames(change.labels)]]" as="labelName">
+ items="[[_computeLabelNames(labels)]]" as="labelName">
<section>
<span class="title">[[labelName]]</span>
<span class="value">
<template is="dom-repeat"
- items="[[_computeLabelValues(labelName, change.labels.*)]]"
+ items="[[_computeLabelValues(labelName, labels.*)]]"
as="label">
<div class="labelValueContainer">
<span>
@@ -344,26 +337,11 @@
</span>
</section>
</template>
- <template is="dom-if" if="[[_showLabelStatus]]">
- <section class="labelStatus">
- <span class="title">Label Status</span>
+ <template is="dom-if" if="[[_showRequirements]]">
+ <section class="requirementsStatus">
+ <span class="title">Submit Status</span>
<span class="value">
- <div hidden$="[[!_isWip]]">
- Work in progress
- </div>
- <div hidden$="[[!_showMissingLabels(missingLabels)]]">
- [[_computeMissingLabelsHeader(missingLabels)]]
- <ul id="missingLabels">
- <template
- is="dom-repeat"
- items="[[missingLabels]]">
- <li>[[item]]</li>
- </template>
- </ul>
- </div>
- <div hidden$="[[_showMissingRequirements(missingLabels, _isWip)]]">
- Ready to submit
- </div>
+ <gr-change-requirements change="[[change]]"></gr-change-requirements>
</span>
</section>
</template>
@@ -379,6 +357,7 @@
</span>
</section>
<gr-endpoint-decorator name="change-metadata-item">
+ <gr-endpoint-param name="labels" value="[[labels]]"></gr-endpoint-param>
<gr-endpoint-param name="change" value="[[change]]"></gr-endpoint-param>
<gr-endpoint-param name="revision" value="[[revision]]"></gr-endpoint-param>
</gr-endpoint-decorator>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 8947e0e..aaada4f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -42,10 +42,13 @@
properties: {
/** @type {?} */
change: Object,
+ labels: {
+ type: Object,
+ notify: true,
+ },
/** @type {?} */
revision: Object,
commitInfo: Object,
- missingLabels: Array,
mutable: Boolean,
/**
* @type {{ note_db_enabled: string }}
@@ -69,9 +72,9 @@
type: Boolean,
computed: '_computeShowReviewersByState(serverConfig)',
},
- _showLabelStatus: {
+ _showRequirements: {
type: Boolean,
- computed: '_computeShowLabelStatus(change)',
+ computed: '_computeShowRequirements(change)',
},
_assignee: Array,
@@ -98,9 +101,14 @@
observers: [
'_changeChanged(change)',
+ '_labelsChanged(change.labels)',
'_assigneeChanged(_assignee.*)',
],
+ _labelsChanged(labels) {
+ this.labels = Object.assign({}, labels) || null;
+ },
+
_changeChanged(change) {
this._assignee = change.assignee ? [change.assignee] : [];
},
@@ -243,17 +251,22 @@
},
_computeTopicReadOnly(mutable, change) {
- return !mutable || !change.actions.topic || !change.actions.topic.enabled;
+ return !mutable ||
+ !change.actions ||
+ !change.actions.topic ||
+ !change.actions.topic.enabled;
},
_computeHashtagReadOnly(mutable, change) {
return !mutable ||
+ !change.actions ||
!change.actions.hashtags ||
!change.actions.hashtags.enabled;
},
_computeAssigneeReadOnly(mutable, change) {
return !mutable ||
+ !change.actions ||
!change.actions.assignee ||
!change.actions.assignee.enabled;
},
@@ -272,6 +285,19 @@
return !!serverConfig.note_db_enabled;
},
+ _computeShowRequirements(change) {
+ if (change.status !== this.ChangeStatus.NEW) {
+ // TODO(maximeg) change this to display the stored
+ // requirements, once it is implemented server-side.
+ return false;
+ }
+ const hasRequirements = !!change.requirements &&
+ Object.keys(change.requirements).length > 0;
+ const hasLabels = !!change.labels &&
+ Object.keys(change.labels).length > 0;
+ return hasRequirements || hasLabels || !!change.work_in_progress;
+ },
+
/**
* A user is able to delete a vote iff the mutable property is true and the
* reviewer that left the vote exists in the list of removable_reviewers
@@ -283,7 +309,9 @@
* change-metadata section is modifiable by the current user.
*/
_computeCanDeleteVote(reviewer, mutable) {
- if (!mutable) { return false; }
+ if (!mutable || !this.change || !this.change.removable_reviewers) {
+ return false;
+ }
for (let i = 0; i < this.change.removable_reviewers.length; i++) {
if (this.change.removable_reviewers[i]._account_id ===
reviewer._account_id) {
@@ -313,6 +341,7 @@
if (!response.ok) { return response; }
const label = this.change.labels[labelName];
const labels = label.all || [];
+ let wasChanged = false;
for (let i = 0; i < labels.length; i++) {
if (labels[i]._account_id === accountID) {
for (const key in label) {
@@ -320,38 +349,24 @@
label[key]._account_id === accountID) {
// Remove special label field, keeping change label values
// in sync with the backend.
- this.set(['change.labels', labelName, key], null);
+ this.change.labels[labelName][key] = null;
+ wasChanged = true;
}
}
- this.splice(['change.labels', labelName, 'all'], i, 1);
+ this.change.labels[labelName].all.splice(i, 1);
+ wasChanged = true;
break;
}
}
+ if (wasChanged) {
+ this.notifyPath('change.labels');
+ }
}).catch(err => {
target.disabled = false;
return;
});
},
- _computeShowLabelStatus(change) {
- const isNewChange = change.status === this.ChangeStatus.NEW;
- const hasLabels = Object.keys(change.labels).length > 0;
- return isNewChange && hasLabels;
- },
-
- _computeMissingLabelsHeader(missingLabels) {
- return 'Needs label' +
- (missingLabels.length > 1 ? 's' : '') + ':';
- },
-
- _showMissingLabels(missingLabels) {
- return !!missingLabels.length;
- },
-
- _showMissingRequirements(missingLabels, workInProgress) {
- return workInProgress || this._showMissingLabels(missingLabels);
- },
-
_computeProjectURL(project) {
return Gerrit.Nav.getUrlForProjectChanges(project);
},
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 68b838f..1330451 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -65,6 +65,43 @@
'Rebase Always');
});
+ test('computed fields requirements', () => {
+ assert.isFalse(element._computeShowRequirements({status: 'MERGED'}));
+ assert.isFalse(element._computeShowRequirements({status: 'ABANDONED'}));
+
+ // No labels and no requirements: submit status is useless
+ assert.isFalse(element._computeShowRequirements({
+ status: 'NEW',
+ labels: {},
+ }));
+
+ // Work in Progress: submit status should be present
+ assert.isTrue(element._computeShowRequirements({
+ status: 'NEW',
+ labels: {},
+ work_in_progress: true,
+ }));
+
+ // We have at least one reason to display Submit Status
+ assert.isTrue(element._computeShowRequirements({
+ status: 'NEW',
+ labels: {
+ Verified: {
+ approved: false,
+ },
+ },
+ requirements: [],
+ }));
+ assert.isTrue(element._computeShowRequirements({
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'OK',
+ }],
+ }));
+ });
+
test('show strategy for open change', () => {
element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {}};
flushAsynchronousOperations();
@@ -92,26 +129,6 @@
assert.isTrue(hasCc());
});
- test('computes submit status', () => {
- let showMissingLabels = false;
- sandbox.stub(element, '_showMissingLabels', () => {
- return showMissingLabels;
- });
- assert.isFalse(element._showMissingRequirements(null, false));
- assert.isTrue(element._showMissingRequirements(null, true));
- showMissingLabels = true;
- assert.isTrue(element._showMissingRequirements(null, false));
- });
-
- test('show missing labels', () => {
- let missingLabels = [];
- assert.isFalse(element._showMissingLabels(missingLabels));
- missingLabels = ['test'];
- assert.isTrue(element._showMissingLabels(missingLabels));
- missingLabels.push('test2');
- assert.isTrue(element._showMissingLabels(missingLabels));
- });
-
test('weblinks use Gerrit.Nav interface', () => {
const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
.returns([{name: 'stubb', url: '#s'}]);
@@ -168,20 +185,6 @@
assert.equal(element._computeWebLinks(element.commitInfo).length, 1);
});
- test('determines whether to show "Ready to Submit" label', () => {
- const showMissingSpy = sandbox.spy(element, '_showMissingRequirements');
- element.missingLabels = ['bojack'];
- element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {
- test: {
- all: [{_account_id: 1, name: 'bojack', value: 1}],
- default_value: 0,
- values: [],
- },
- }};
- flushAsynchronousOperations();
- assert.isTrue(showMissingSpy.called);
- });
-
test('_computeShowUploader test for uploader', () => {
const change = {
change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
@@ -523,36 +526,26 @@
assert.isFalse(button.hasAttribute('hidden'));
});
- test('deletes votes', done => {
- const deleteStub = sandbox.stub(element.$.restAPI, 'deleteVote')
- .returns(Promise.resolve({ok: true}));
+ test('deletes votes', () => {
+ const deleteResponse = Promise.resolve({ok: true});
+ const deleteStub = sandbox.stub(
+ element.$.restAPI, 'deleteVote').returns(deleteResponse);
- element.change.removable_reviewers = [
- {
- _account_id: 1,
- name: 'bojack',
- },
- ];
+ element.change.removable_reviewers = [{
+ _account_id: 1,
+ name: 'bojack',
+ }];
element.change.labels.test.recommended = {_account_id: 1};
element.mutable = true;
- flushAsynchronousOperations();
const chip = element.$$('gr-account-chip');
const button = chip.$$('gr-button');
-
- const spliceStub = sandbox.stub(element, 'splice', (path, index,
- length) => {
- assert.isFalse(chip.disabled);
- assert.deepEqual(path, ['change.labels', 'test', 'all']);
- assert.equal(index, 0);
- assert.equal(length, 1);
- assert.notOk(element.change.labels.test.recommended);
- assert.isTrue(deleteStub.calledWithExactly(42, 1, 'test'));
- spliceStub.restore();
- done();
- });
-
MockInteractions.tap(button);
assert.isTrue(chip.disabled);
+ return deleteResponse.then(() => {
+ assert.isFalse(chip.disabled);
+ assert.notOk(element.change.labels.test.recommended);
+ assert.isTrue(deleteStub.calledWithExactly(42, 1, 'test'));
+ });
});
test('changing topic', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
new file mode 100644
index 0000000..536b943
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.html
@@ -0,0 +1,75 @@
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-change-requirements">
+ <template strip-whitespace>
+ <style include="shared-styles">
+ .status {
+ display: inline-block;
+ font-weight: initial;
+ text-align: center;
+ width: 1em;
+ }
+ .unsatisfied .status {
+ color: #FFA62F;
+ }
+ .satisfied .status {
+ color: #388E3C;
+ }
+ .requirement {
+ padding: .1em .3em;
+ }
+ .requirementContainer:not(:first-of-type) {
+ margin-top: .25em;
+ }
+ .labelName, .changeIsWip {
+ font-weight: bold;
+ }
+ </style>
+ <template is="dom-if" if="[[_showWip]]">
+ <div class="requirement unsatisfied changeIsWip">
+ <span class="status">⧗</span>
+ Work in Progress
+ </div>
+ </template>
+ <template is="dom-if" if="[[_showLabels]]">
+ <template
+ is="dom-repeat"
+ items="[[labels]]">
+ <div class$="requirement [[item.style]]">
+ <span class="status">[[item.status]]</span>
+ Label <span class="labelName">[[item.label]]</span>
+ </div>
+ </template>
+ </template>
+ <template
+ is="dom-repeat"
+ items="[[requirements]]">
+ <div class$="requirement [[_computeRequirementClass(item.satisfied)]]">
+ <span class="status">
+ [[_computeRequirementStatus(item.satisfied)]]
+ </span>
+ [[item.fallback_text]]
+ </div>
+ </template>
+ </template>
+ <script src="gr-change-requirements.js"></script>
+</dom-module>
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
new file mode 100644
index 0000000..884e9cd
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
@@ -0,0 +1,102 @@
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-change-requirements',
+
+ properties: {
+ /** @type {?} */
+ change: Object,
+ requirements: {
+ type: Array,
+ computed: '_computeRequirements(change)',
+ },
+ labels: {
+ type: Array,
+ computed: '_computeLabels(change)',
+ },
+ _showWip: {
+ type: Boolean,
+ computed: '_computeShowWip(change)',
+ },
+ _showLabels: {
+ type: Boolean,
+ computed: '_computeShowLabelStatus(change)',
+ },
+ },
+
+ behaviors: [
+ Gerrit.RESTClientBehavior,
+ ],
+
+ _computeShowLabelStatus(change) {
+ return change.status === this.ChangeStatus.NEW;
+ },
+
+ _computeShowWip(change) {
+ return change.work_in_progress;
+ },
+
+ _computeRequirements(change) {
+ const _requirements = [];
+
+ if (change.requirements) {
+ for (const requirement of change.requirements) {
+ requirement.satisfied = requirement.status === 'OK';
+ _requirements.push(requirement);
+ }
+ }
+
+ return _requirements;
+ },
+
+ _computeLabels(change) {
+ const labels = change.labels;
+ const _labels = [];
+
+ for (const label in labels) {
+ if (!labels.hasOwnProperty(label)) { continue; }
+ const obj = labels[label];
+ if (obj.optional) { continue; }
+
+ const status = this._computeRequirementStatus(obj.approved);
+ const style = this._computeRequirementClass(obj.approved);
+ _labels.push({label, status, style});
+ }
+
+ return _labels;
+ },
+
+ _computeRequirementClass(requirementStatus) {
+ if (requirementStatus) {
+ return 'satisfied';
+ } else {
+ return 'unsatisfied';
+ }
+ },
+
+ _computeRequirementStatus(requirementStatus) {
+ if (requirementStatus) {
+ return '✓';
+ } else {
+ return '⧗';
+ }
+ },
+ });
+})();
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
new file mode 100644
index 0000000..560a33d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
@@ -0,0 +1,129 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-change-requirements</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-change-requirements.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-change-requirements></gr-change-requirements>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-change-metadata tests', () => {
+ let element;
+
+ setup(() => {
+ element = fixture('basic');
+ });
+
+ test('computed fields', () => {
+ assert.isTrue(element._computeShowLabelStatus({status: 'NEW'}));
+ assert.isFalse(element._computeShowLabelStatus({status: 'MERGED'}));
+ assert.isFalse(element._computeShowLabelStatus({status: 'ABANDONED'}));
+
+ assert.isTrue(element._computeShowWip({work_in_progress: true}));
+ assert.isFalse(element._computeShowWip({work_in_progress: false}));
+
+ assert.equal(element._computeRequirementClass(true), 'satisfied');
+ assert.equal(element._computeRequirementClass(false), 'unsatisfied');
+
+ assert.equal(element._computeRequirementStatus(true), '✓');
+ assert.equal(element._computeRequirementStatus(false), '⧗');
+ });
+
+ test('properly converts satisfied labels', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {
+ Verified: {
+ approved: true,
+ },
+ },
+ requirements: [],
+ };
+ flushAsynchronousOperations();
+
+ const labelName = element.$$('.satisfied .labelName');
+ assert.ok(labelName);
+ assert.isFalse(labelName.hasAttribute('hidden'));
+ assert.equal(labelName.innerHTML, 'Verified');
+ });
+
+ test('properly converts unsatisfied labels', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {
+ Verified: {
+ approved: false,
+ },
+ },
+ };
+ flushAsynchronousOperations();
+
+ const labelName = element.$$('.unsatisfied .labelName');
+ assert.ok(labelName);
+ assert.isFalse(labelName.hasAttribute('hidden'));
+ assert.equal(labelName.innerHTML, 'Verified');
+ });
+
+ test('properly displays Work In Progress', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [],
+ work_in_progress: true,
+ };
+ flushAsynchronousOperations();
+
+ const changeIsWip = element.$$('.changeIsWip.unsatisfied');
+ assert.ok(changeIsWip);
+ assert.isFalse(changeIsWip.hasAttribute('hidden'));
+ assert.notEqual(changeIsWip.innerHTML.indexOf('Work in Progress'), -1);
+ });
+
+
+ test('properly displays a satisfied requirement', () => {
+ element.change = {
+ status: 'NEW',
+ labels: {},
+ requirements: [{
+ fallback_text: 'Resolve all comments',
+ status: 'OK',
+ }],
+ };
+ flushAsynchronousOperations();
+
+ const satisfiedRequirement = element.$$('.satisfied');
+ assert.ok(satisfiedRequirement);
+ assert.isFalse(satisfiedRequirement.hasAttribute('hidden'));
+
+ // Extract the content of the text node (second element, after the span)
+ const textNode = satisfiedRequirement.childNodes[1].nodeValue.trim();
+ assert.equal(textNode, 'Resolve all comments');
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 1827ef6..0688435 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -22,6 +22,7 @@
<link rel="import" href="../../../bower_components/paper-tabs/paper-tabs.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
<link rel="import" href="../../diff/gr-diff-preferences/gr-diff-preferences.html">
<link rel="import" href="../../edit/gr-edit-constants.html">
@@ -58,13 +59,13 @@
background-color: var(--view-background-color);
}
.container.loading {
- color: #666;
+ color: var(--deemphasized-text-color);
padding: 1em var(--default-horizontal-margin);
}
.header {
align-items: center;
- background-color: #fafafa;
- border-bottom: 1px solid #ddd;
+ background-color: var(--table-header-background-color);
+ border-bottom: 1px solid var(--border-color);
display: flex;
padding: .55em var(--default-horizontal-margin);
z-index: 99; /* Less than gr-overlay's backdrop */
@@ -119,7 +120,7 @@
padding: 0 var(--default-horizontal-margin);
}
.changeId {
- color: #666;
+ color: var(--deemphasized-text-color);
font-family: var(--font-family);
margin-top: 1em;
}
@@ -128,7 +129,7 @@
padding-right: 1em;
}
.changeMetadata {
- border-right: 1px solid #ddd;
+ border-right: 1px solid var(--border-color);
font-size: .95rem;
padding: 1em 0;
}
@@ -189,7 +190,7 @@
}
hr {
border: 0;
- border-top: 1px solid #ddd;
+ border-top: 1px solid var(--border-color);
height: 0;
margin-bottom: 1em;
}
@@ -241,14 +242,14 @@
margin-right: -5px;
}
paper-tabs {
- background-color: #fafafa;
- border-top: 1px solid #ddd;
+ background-color: var(--table-header-background-color);
+ border-top: 1px solid var(--border-color);
height: 3rem;
- --paper-tabs-selection-bar-color: var(--color-link);
+ --paper-tabs-selection-bar-color: var(--link-color);
}
paper-tab {
max-width: 15rem;
- --paper-tab-ink: var(--color-link);
+ --paper-tab-ink: var(--link-color);
}
gr-thread-list,
gr-messages-list {
@@ -406,7 +407,6 @@
revision="[[_selectedRevision]]"
commit-info="[[_commitInfo]]"
server-config="[[_serverConfig]]"
- missing-labels="[[_missingLabels]]"
mutable="[[_loggedIn]]"
parent-is-current="[[_parentIsCurrent]]"
on-show-reply-dialog="_handleShowReplyDialog">
@@ -425,7 +425,7 @@
id="replyBtn"
class="reply"
hidden$="[[!_loggedIn]]"
- secondary
+ primary
disabled="[[_replyDisabled]]"
on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button>
</div>
@@ -616,6 +616,7 @@
<gr-js-api-interface id="jsAPI"></gr-js-api-interface>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-comment-api id="commentAPI"></gr-comment-api>
+ <gr-reporting id="reporting"></gr-reporting>
</template>
<script src="gr-change-view.js"></script>
</dom-module>
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 e999911..29ffec8 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
@@ -174,11 +174,6 @@
},
_loading: Boolean,
/** @type {?} */
- _missingLabels: {
- type: Array,
- computed: '_computeMissingLabels(_change.labels)',
- },
- /** @type {?} */
_projectConfig: Object,
_rebaseOnCurrent: Boolean,
_replyButtonLabel: {
@@ -225,6 +220,7 @@
observer: '_updateToggleContainerClass',
},
_parentIsCurrent: Boolean,
+ _submitEnabled: Boolean,
/** @type {?} */
_mergeable: {
@@ -383,18 +379,6 @@
this._editingCommitMessage = false;
},
- _computeMissingLabels(labels) {
- const missingLabels = [];
- for (const label in labels) {
- if (!labels.hasOwnProperty(label)) { continue; }
- const obj = labels[label];
- if (!obj.optional && !obj.approved) {
- missingLabels.push(label);
- }
- }
- return missingLabels;
- },
-
_computeChangeStatusChips(change, mergeable) {
// Show no chips until mergeability is loaded.
if (mergeable === null || mergeable === undefined) { return []; }
@@ -402,6 +386,7 @@
const options = {
includeDerived: true,
mergeable: !!mergeable,
+ submitEnabled: this._submitEnabled,
};
return this.changeStatuses(change, options);
},
@@ -618,6 +603,10 @@
return;
}
+ if (value.changeNum && value.project) {
+ this.$.restAPI.setInProjectLookup(value.changeNum, value.project);
+ }
+
const patchChanged = this._patchRange &&
(value.patchNum !== undefined && value.basePatchNum !== undefined) &&
(this._patchRange.patchNum !== value.patchNum ||
@@ -1102,6 +1091,10 @@
} else {
this._latestCommitMessage = null;
}
+
+ // Update the submit enabled based on current revision.
+ this._submitEnabled = this._isSubmitEnabled(currentRevision);
+
const lineHeight = getComputedStyle(this).lineHeight;
// Slice returns a number as a string, convert to an int.
@@ -1130,6 +1123,11 @@
});
},
+ _isSubmitEnabled(currentRevision) {
+ return !!(currentRevision.actions && currentRevision.actions.submit &&
+ currentRevision.actions.submit.enabled);
+ },
+
_getEdit() {
return this.$.restAPI.getChangeEdit(this._changeNum, true);
},
@@ -1212,8 +1210,10 @@
this._reloadComments();
+ let reloadPromise;
+
if (this._patchRange.patchNum) {
- return Promise.all([
+ reloadPromise = Promise.all([
this._reloadPatchNumDependentResources(),
detailCompletes,
]).then(() => {
@@ -1224,7 +1224,7 @@
});
} else {
// The patch number is reliant on the change detail request.
- return detailCompletes.then(() => {
+ reloadPromise = detailCompletes.then(() => {
this.$.fileList.reload();
if (!this._latestCommitMessage) {
this._getLatestCommitMessage();
@@ -1232,6 +1232,10 @@
return this._getMergeability();
});
}
+
+ return reloadPromise.then(() => {
+ this.$.reporting.changeDisplayed();
+ });
},
/**
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index f377277..a2a2de7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -464,22 +464,6 @@
assert.equal(statusChips.length, 2);
});
- test('_computeMissingLabels', () => {
- let labels = {};
- assert.equal(element._computeMissingLabels(labels).length, 0);
- labels = {test: {}};
- assert.deepEqual(element._computeMissingLabels(labels), ['test']);
- labels.test.approved = true;
- assert.equal(element._computeMissingLabels(labels).length, 0);
- labels.test.approved = false;
- labels.test.optional = true;
- assert.equal(element._computeMissingLabels(labels).length, 0);
- labels.test.optional = false;
- labels.test2 = {};
- assert.deepEqual(element._computeMissingLabels(labels),
- ['test', 'test2']);
- });
-
test('diff preferences open when open-diff-prefs is fired', () => {
const overlayOpenStub = sandbox.stub(element.$.fileList,
'openDiffPrefs');
@@ -540,6 +524,14 @@
assert.isTrue(element._parentIsCurrent);
});
+ test('_isSubmitEnabled', () => {
+ assert.isFalse(element._isSubmitEnabled({}));
+ assert.isFalse(element._isSubmitEnabled({actions: {}}));
+ assert.isFalse(element._isSubmitEnabled({actions: {submit: {}}}));
+ assert.isTrue(element._isSubmitEnabled(
+ {actions: {submit: {enabled: true}}}));
+ });
+
test('_updateRebaseAction sets _parentIsCurrent on no rebase', () => {
const currentRevisionActions = {
cherrypick: {
@@ -1603,8 +1595,6 @@
fireEdit = () => {
element.$.actions.dispatchEvent(new CustomEvent('edit-tap'));
};
- sandbox.stub(element.$.metadata, '_computeShowLabelStatus');
- sandbox.stub(element.$.metadata, '_computeLabelNames');
navigateToChangeStub.restore();
element._change = {revisions: {rev1: {_number: 1}}};
@@ -1656,7 +1646,6 @@
});
test('_handleStopEditTap', done => {
- sandbox.stub(element.$.metadata, '_computeShowLabelStatus');
sandbox.stub(element.$.metadata, '_computeLabelNames');
navigateToChangeStub.restore();
sandbox.stub(element, 'computeLatestPatchNum').returns(1);
@@ -1729,5 +1718,18 @@
});
});
});
+
+ test('_paramsChanged sets in projectLookup', () => {
+ sandbox.stub(element.$.relatedChanges, 'reload');
+ sandbox.stub(element, '_reload').returns(Promise.resolve());
+ const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+ element._paramsChanged({
+ view: Gerrit.Nav.View.CHANGE,
+ changeNum: 101,
+ project: 'test-project',
+ });
+ assert.isTrue(setStub.calledOnce);
+ assert.isTrue(setStub.calledWith(101, 'test-project'));
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index e0362c8..a3d1ffa 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -36,7 +36,7 @@
word-wrap: break-word;
}
.file {
- border-top: 1px solid #ddd;
+ border-top: 1px solid var(--border-color);
font-family: var(--font-family-bold);
margin: 10px 0 3px;
padding: 10px 0 5px;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
index c086b9a..e420312 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
@@ -46,7 +46,7 @@
width: 73ch; /* Add a char to account for the border. */
--iron-autogrow-textarea {
- border: 1px solid #cdcdcd;
+ border: 1px solid var(--border-color);
box-sizing: border-box;
font-family: var(--monospace-font-family);
}
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
index 8ccf15b..07d9b83 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
@@ -41,7 +41,7 @@
width: 73ch; /* Add a char to account for the border. */
--iron-autogrow-textarea {
- border: 1px solid #cdcdcd;
+ border: 1px solid var(--border-color);
box-sizing: border-box;
font-family: var(--monospace-font-family);
}
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index 122f1f9..3dc556d 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -41,12 +41,12 @@
text-decoration: none;
}
.patchInfoOldPatchSet.patchInfo-header {
- background-color: #fff9c4;
+ background-color: var(--emphasis-color);
}
.patchInfo-header {
align-items: center;
- background-color: #fafafa;
- border-top: 1px solid #ddd;
+ background-color: var(--table-header-background-color);
+ border-top: 1px solid var(--border-color);
display: flex;
padding: 6px var(--default-horizontal-margin);
}
@@ -105,7 +105,7 @@
display: none;
}
gr-linked-chip {
- --linked-chip-text-color: black;
+ --linked-chip-text-color: var(--primary-text-color);
}
.expanded #collapseBtn,
.openFile .fileViewActions {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 41818d1..da7a16a 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -44,7 +44,7 @@
}
.row {
align-items: center;
- border-top: 1px solid #ddd;
+ border-top: 1px solid var(--border-color);
display: flex;
min-height: 2.25em;
padding: .2em var(--default-horizontal-margin) .2em calc(var(--default-horizontal-margin) - .35rem);
@@ -89,8 +89,7 @@
cursor: pointer;
}
.file-row.expanded {
- background-color: #eeeeee;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color);
position: -webkit-sticky;
position: sticky;
top: 0;
@@ -99,14 +98,14 @@
z-index: 1;
}
.file-row:hover {
- background-color: #eeeeee;
+ background-color: var(--hover-background-color);
}
- .row {
- /* Needed to provide a spacer for the selected cursor. */
- border-left: .35rem solid transparent;
+ .file-row.selected {
+ background-color: var(--selection-background-color);
}
- .row.selected {
- border-left: .35rem solid var(--color-link);
+ .file-row.expanded,
+ .file-row.expanded:hover {
+ background-color: var(--expanded-background-color);
}
.path {
cursor: pointer;
@@ -173,13 +172,13 @@
text-align: right;
}
.warning {
- color: #666;
+ color: var(--deemphasized-text-color);
}
input.show-hide {
display: none;
}
label.show-hide {
- color: var(--color-link);
+ color: var(--link-color);
cursor: pointer;
display: block;
font-size: var(--font-size-small);
@@ -204,7 +203,7 @@
width: 15em;
}
.reviewed label {
- color: var(--color-link);
+ color: var(--link-color);
opacity: 0;
justify-content: flex-end;
width: 100%;
@@ -225,7 +224,7 @@
display: none;
}
.reviewedLabel {
- color: rgba(0, 0, 0, .54);
+ color: var(--deemphasized-text-color);
margin-right: 1em;
opacity: 0;
}
@@ -250,7 +249,7 @@
display: block;
}
.row.selected {
- background-color: #fff;
+ background-color: var(--view-background-color);
}
.stats {
display: none;
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 91c9006..c978281 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
@@ -32,6 +32,7 @@
D: 'Deleted',
R: 'Renamed',
W: 'Rewritten',
+ U: 'Unchanged',
};
const Defs = {};
@@ -97,8 +98,10 @@
value: GrFileListConstants.FilesExpandedState.NONE,
notify: true,
},
+ _filesByPath: Object,
_files: {
type: Array,
+ computed: '_computeFiles(_filesByPath, changeComments, patchRange)',
observer: '_filesChanged',
value() { return []; },
},
@@ -137,6 +140,7 @@
type: Boolean,
computed: '_shouldHideBinaryChangeTotals(_patchChange)',
},
+
_shownFiles: {
type: Array,
computed: '_computeFilesShown(numFilesShown, _files.*)',
@@ -229,8 +233,8 @@
this.collapseAllDiffs();
const promises = [];
- promises.push(this._getFiles().then(files => {
- this._files = files;
+ promises.push(this._getFiles().then(filesByPath => {
+ this._filesByPath = filesByPath;
}));
promises.push(this._getLoggedIn().then(loggedIn => {
return this._loggedIn = loggedIn;
@@ -449,11 +453,30 @@
},
_getFiles() {
- return this.$.restAPI.getChangeFilesAsSpeciallySortedArray(
+ return this.$.restAPI.getChangeOrEditFiles(
this.changeNum, this.patchRange);
},
/**
+ * The closure compiler doesn't realize this.specialFilePathCompare is
+ * valid.
+ * @suppress {checkTypes}
+ */
+ _normalizeChangeFilesResponse(response) {
+ if (!response) { return []; }
+ const paths = Object.keys(response).sort(this.specialFilePathCompare);
+ const files = [];
+ for (let i = 0; i < paths.length; i++) {
+ const info = response[paths[i]];
+ info.__path = paths[i];
+ info.lines_inserted = info.lines_inserted || 0;
+ info.lines_deleted = info.lines_deleted || 0;
+ files.push(info);
+ }
+ return files;
+ },
+
+ /**
* Handle all events from the file list dom-repeat so event handleers don't
* have to get registered for potentially very long lists.
*/
@@ -748,6 +771,18 @@
'gr-icons:expand-less' : 'gr-icons:expand-more';
},
+ _computeFiles(filesByPath, changeComments, patchRange) {
+ const commentedPaths = changeComments.getPaths(patchRange);
+ const files = Object.assign({}, filesByPath);
+ Object.keys(commentedPaths).forEach(commentedPath => {
+ if (files.hasOwnProperty(commentedPath)) {
+ return;
+ }
+ files[commentedPath] = {status: 'U'};
+ });
+ return this._normalizeChangeFilesResponse(files);
+ },
+
_computeFilesShown(numFilesShown, files) {
const filesShown = files.base.slice(0, numFilesShown);
this.fire('files-shown-changed', {length: filesShown.length});
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 e2a3bf4..3f656ae 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
@@ -88,6 +88,10 @@
});
element.diffPrefs = {};
element.numFilesShown = 200;
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
saveStub = sandbox.stub(element, '_saveReviewedState',
() => { return Promise.resolve(); });
});
@@ -98,9 +102,12 @@
test('correct number of files are shown', () => {
element.fileListIncrement = 300;
- element._files = _.times(500, i => {
- return {__path: '/file' + i, lines_inserted: 9};
- });
+ element._filesByPath = _.range(500)
+ .reduce((_filesByPath, i) => {
+ _filesByPath['/file' + i] = {lines_inserted: 9};
+ return _filesByPath;
+ }, {});
+
flushAsynchronousOperations();
assert.equal(
Polymer.dom(element.root).querySelectorAll('.file-row').length,
@@ -121,23 +128,24 @@
});
test('calculate totals for patch number', () => {
- element._files = [
- {__path: '/COMMIT_MSG', lines_inserted: 9},
- {
- __path: 'file_added_in_rev2.txt',
+ element._filesByPath = {
+ '/COMMIT_MSG': {
+ lines_inserted: 9,
+ },
+ 'file_added_in_rev2.txt': {
lines_inserted: 1,
lines_deleted: 1,
size_delta: 10,
size: 100,
},
- {
- __path: 'myfile.txt',
+ 'myfile.txt': {
lines_inserted: 1,
lines_deleted: 1,
size_delta: 10,
size: 100,
},
- ];
+ };
+
assert.deepEqual(element._patchChange, {
inserted: 2,
deleted: 2,
@@ -149,11 +157,20 @@
assert.isFalse(element._hideChangeTotals);
// Test with a commit message that isn't the first file.
- element._files = [
- {__path: 'file_added_in_rev2.txt', lines_inserted: 1, lines_deleted: 1},
- {__path: '/COMMIT_MSG', lines_inserted: 9},
- {__path: 'myfile.txt', lines_inserted: 1, lines_deleted: 1},
- ];
+ element._filesByPath = {
+ 'file_added_in_rev2.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ },
+ '/COMMIT_MSG': {
+ lines_inserted: 9,
+ },
+ 'myfile.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ },
+ };
+
assert.deepEqual(element._patchChange, {
inserted: 2,
deleted: 2,
@@ -165,10 +182,17 @@
assert.isFalse(element._hideChangeTotals);
// Test with no commit message.
- element._files = [
- {__path: 'file_added_in_rev2.txt', lines_inserted: 1, lines_deleted: 1},
- {__path: 'myfile.txt', lines_inserted: 1, lines_deleted: 1},
- ];
+ element._filesByPath = {
+ 'file_added_in_rev2.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ },
+ 'myfile.txt': {
+ lines_inserted: 1,
+ lines_deleted: 1,
+ },
+ };
+
assert.deepEqual(element._patchChange, {
inserted: 2,
deleted: 2,
@@ -180,10 +204,10 @@
assert.isFalse(element._hideChangeTotals);
// Test with files missing either lines_inserted or lines_deleted.
- element._files = [
- {__path: 'file_added_in_rev2.txt', lines_inserted: 1},
- {__path: 'myfile.txt', lines_deleted: 1},
- ];
+ element._filesByPath = {
+ 'file_added_in_rev2.txt': {lines_inserted: 1},
+ 'myfile.txt': {lines_deleted: 1},
+ };
assert.deepEqual(element._patchChange, {
inserted: 1,
deleted: 1,
@@ -196,11 +220,11 @@
});
test('binary only files', () => {
- element._files = [
- {__path: '/COMMIT_MSG', lines_inserted: 9},
- {__path: 'file_binary', binary: true, size_delta: 10, size: 100},
- {__path: 'file_binary', binary: true, size_delta: -5, size: 120},
- ];
+ element._filesByPath = {
+ '/COMMIT_MSG': {lines_inserted: 9},
+ 'file_binary_1': {binary: true, size_delta: 10, size: 100},
+ 'file_binary_2': {binary: true, size_delta: -5, size: 120},
+ };
assert.deepEqual(element._patchChange, {
inserted: 0,
deleted: 0,
@@ -213,13 +237,13 @@
});
test('binary and regular files', () => {
- element._files = [
- {__path: '/COMMIT_MSG', lines_inserted: 9},
- {__path: 'file_binary', binary: true, size_delta: 10, size: 100},
- {__path: 'file_binary', binary: true, size_delta: -5, size: 120},
- {__path: 'myfile.txt', lines_deleted: 5, size_delta: -10, size: 100},
- {__path: 'myfile2.txt', lines_inserted: 10},
- ];
+ element._filesByPath = {
+ '/COMMIT_MSG': {lines_inserted: 9},
+ 'file_binary_1': {binary: true, size_delta: 10, size: 100},
+ 'file_binary_2': {binary: true, size_delta: -5, size: 120},
+ 'myfile.txt': {lines_deleted: 5, size_delta: -10, size: 100},
+ 'myfile2.txt': {lines_inserted: 10},
+ };
assert.deepEqual(element._patchChange, {
inserted: 10,
deleted: 5,
@@ -418,11 +442,11 @@
suite('keyboard shortcuts', () => {
setup(() => {
- element._files = [
- {__path: '/COMMIT_MSG'},
- {__path: 'file_added_in_rev2.txt'},
- {__path: 'myfile.txt'},
- ];
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ 'file_added_in_rev2.txt': {},
+ 'myfile.txt': {},
+ };
element.changeNum = '42';
element.patchRange = {
basePatchNum: 'PARENT',
@@ -631,11 +655,11 @@
});
test('file review status', () => {
- element._files = [
- {__path: '/COMMIT_MSG'},
- {__path: 'file_added_in_rev2.txt'},
- {__path: 'myfile.txt'},
- ];
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ 'file_added_in_rev2.txt': {},
+ 'myfile.txt': {},
+ };
element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
element._loggedIn = true;
element.changeNum = '42';
@@ -677,11 +701,11 @@
});
test('_handleFileListTap', () => {
- element._files = [
- {__path: '/COMMIT_MSG'},
- {__path: 'f1.txt'},
- {__path: 'f2.txt'},
- ];
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ 'f1.txt': {},
+ 'f2.txt': {},
+ };
element.changeNum = '42';
element.patchRange = {
basePatchNum: 'PARENT',
@@ -739,9 +763,9 @@
});
test('checkbox shows/hides diff inline', () => {
- element._files = [
- {__path: 'myfile.txt'},
- ];
+ element._filesByPath = {
+ 'myfile.txt': {},
+ };
element.changeNum = '42';
element.patchRange = {
basePatchNum: 'PARENT',
@@ -764,9 +788,9 @@
});
test('diff mode correctly toggles the diffs', () => {
- element._files = [
- {__path: 'myfile.txt'},
- ];
+ element._filesByPath = {
+ 'myfile.txt': {},
+ };
element.changeNum = '42';
element.patchRange = {
basePatchNum: 'PARENT',
@@ -790,16 +814,16 @@
});
test('expanded attribute not set on path when not expanded', () => {
- element._files = [
- {__path: '/COMMIT_MSG'},
- ];
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ };
assert.isNotOk(element.$$('.expanded'));
});
test('tapping row ignores links', () => {
- element._files = [
- {__path: '/COMMIT_MSG'},
- ];
+ element._filesByPath = {
+ '/COMMIT_MSG': {},
+ };
element.changeNum = '42';
element.patchRange = {
basePatchNum: 'PARENT',
@@ -824,7 +848,7 @@
test('_togglePathExpanded', () => {
const path = 'path/to/my/file.txt';
- element._files = [{__path: path}];
+ element._filesByPath = {[path]: {}};
const renderSpy = sandbox.spy(element, '_renderInOrder');
const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs');
@@ -852,7 +876,7 @@
'handleDiffUpdate');
const path = 'path/to/my/file.txt';
- element._files = [{__path: path}];
+ element._filesByPath = {[path]: {}};
element.expandAllDiffs();
flushAsynchronousOperations();
assert.isTrue(element._showInlineDiffs);
@@ -894,7 +918,10 @@
});
test('filesExpanded value updates to correct enum', () => {
- element._files = [{__path: 'foo.bar'}, {__path: 'baz.bar'}];
+ element._filesByPath = {
+ 'foo.bar': {},
+ 'baz.bar': {},
+ };
flushAsynchronousOperations();
assert.equal(element.filesExpanded,
GrFileListConstants.FilesExpandedState.NONE);
@@ -1000,7 +1027,7 @@
test('_loadingChanged fired from reload in debouncer', done => {
element.changeNum = 123;
element.patchRange = {patchNum: 12};
- element._files = [{__path: 'foo.bar'}];
+ element._filesByPath = {'foo.bar': {}};
element.reload().then(() => {
assert.isFalse(element._loading);
@@ -1027,7 +1054,7 @@
const urlStub = sandbox.stub(element, '_computeDiffURL');
element.change = {_number: 123};
element.patchRange = {patchNum: undefined, basePatchNum: 'PARENT'};
- element._files = [{__path: 'foo/bar.cpp'}];
+ element._filesByPath = {'foo/bar.cpp': {}};
element.editMode = false;
flush(() => {
assert.isFalse(urlStub.called);
@@ -1278,23 +1305,21 @@
});
element.numFilesShown = 75;
element.selectedIndex = 0;
- element._files = [
- {__path: '/COMMIT_MSG', lines_inserted: 9},
- {
- __path: 'file_added_in_rev2.txt',
+ element._filesByPath = {
+ '/COMMIT_MSG': {lines_inserted: 9},
+ 'file_added_in_rev2.txt': {
lines_inserted: 1,
lines_deleted: 1,
size_delta: 10,
size: 100,
},
- {
- __path: 'myfile.txt',
+ 'myfile.txt': {
lines_inserted: 1,
lines_deleted: 1,
size_delta: 10,
size: 100,
},
- ];
+ };
element._reviewed = ['/COMMIT_MSG', 'myfile.txt'];
element._loggedIn = true;
element.changeNum = '42';
@@ -1457,14 +1482,14 @@
});
test('_openSelectedFile behavior', () => {
- const _files = element._files;
- element.set('_files', []);
+ const _filesByPath = element._filesByPath;
+ element.set('_filesByPath', {});
const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff');
// Noop when there are no files.
element._openSelectedFile();
assert.isFalse(navStub.called);
- element.set('_files', _files);
+ element.set('_filesByPath', _filesByPath);
flushAsynchronousOperations();
// Navigates when a file is selected.
element._openSelectedFile();
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
index ab77bf5..a07c724f 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
@@ -29,8 +29,8 @@
padding: 4.5em 1em 1em 1em;
}
header {
- background: #fff;
- border-bottom: 1px solid #cdcdcd;
+ background: var(--view-background-color);
+ border-bottom: 1px solid var(--border-color);
left: 0;
padding: 1em;
position: absolute;
@@ -59,7 +59,7 @@
}
ul li {
border-radius: .2em;
- background: #eee;
+ background: var(--header-background-color);
display: inline-block;
margin: 0 .2em .4em .2em;
padding: .2em .4em;
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
index 54e8ae2..aae21c0 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
@@ -39,13 +39,13 @@
width: 20%;
}
.labelMessage {
- color: #666;
+ color: var(--deemphasized-text-color);
}
.placeholder::before {
content: ' ';
}
.selectedValueText {
- color: rgba(0, 0, 0, .54);
+ color: var(--deemphasized-text-color);
font-style: italic;
margin: 0 .5em 0 .5em;
}
@@ -58,25 +58,25 @@
gr-button {
min-width: 40px;
--gr-button: {
+ background-color: var(--button-background-color, #f5f5f5);
+ color: var(--primary-text-color);
padding: .2em .85em;
@apply(--vote-chip-styles);
}
- --gr-button-background: var(--button-background-color, #f5f5f5);
- --gr-button-color: black;
}
- iron-selector > gr-button.iron-selected.max {
+ gr-button.iron-selected.max {
--button-background-color: var(--vote-color-max);
}
- iron-selector > gr-button.iron-selected.positive {
+ gr-button.iron-selected.positive {
--button-background-color: var(--vote-color-positive);
}
- iron-selector > gr-button.iron-selected.min {
+ gr-button.iron-selected.min {
--button-background-color: var(--vote-color-min);
}
- iron-selector > gr-button.iron-selected.negative {
+ gr-button.iron-selected.negative {
--button-background-color: var(--vote-color-negative);
}
- iron-selector > gr-button.iron-selected.neutral {
+ gr-button.iron-selected.neutral {
--button-background-color: var(--vote-color-neutral);
}
.placeholder {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index 5a2ac1b..a73faa4 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -31,7 +31,7 @@
<style include="gr-voting-styles"></style>
<style include="shared-styles">
:host {
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color);
display: block;
position: relative;
cursor: pointer;
@@ -48,7 +48,7 @@
}
.collapsed .contentContainer {
align-items: baseline;
- color: #777;
+ color: var(--deemphasized-text-color);
display: flex;
white-space: nowrap;
}
@@ -119,7 +119,7 @@
position: static;
}
.collapsed .author {
- color: var(--default-text-color);
+ color: var(--primary-text-color);
margin-right: .4em;
}
.expanded .author {
@@ -127,7 +127,7 @@
margin-bottom: .4em;
}
.date {
- color: #666;
+ color: var(--deemphasized-text-color);
position: absolute;
right: var(--default-horizontal-margin);
top: 10px;
@@ -138,7 +138,7 @@
.score {
border: 1px solid rgba(0,0,0,.12);
border-radius: 3px;
- color: #000;
+ color: var(--primary-text-color);
display: inline-block;
margin: -.1em 0;
padding: 0 .1em;
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
index df635bf..0a7dacc 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -32,9 +32,9 @@
}
.header {
align-items: center;
- background-color: #fafafa;
- border-bottom: 1px solid #ddd;
- border-top: 1px solid #ddd;
+ background-color: var(--table-header-background-color);
+ border-bottom: 1px solid var(--border-color);
+ border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
min-height: 3.2em;
@@ -47,12 +47,12 @@
animation: 3s fadeOut;
}
@keyframes fadeOut {
- 0% { background-color: #fff9c4; }
- 100% { background-color: #fff; }
+ 0% { background-color: var(--emphasis-color); }
+ 100% { background-color: var(--view-background-color); }
}
#messageControlsContainer {
align-items: center;
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color);
display: flex;
height: 2.25em;
justify-content: center;
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
index e78a228..e9d8ce8 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -65,11 +65,11 @@
display: inline-block;
}
.strikethrough {
- color: #666;
+ color: var(--deemphasized-text-color);
text-decoration: line-through;
}
.status {
- color: #666;
+ color: var(--deemphasized-text-color);
font-family: var(--font-family-bold);
margin-left: .25em;
}
@@ -99,7 +99,7 @@
}
hr {
border: 0;
- border-top: 1px solid #ddd;
+ border-top: 1px solid var(--border-color);
height: 0;
margin-bottom: 1em;
}
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
index af3acc4..eb06b0f 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -110,8 +110,6 @@
});
teardown(() => {
- Gerrit._pluginsPending = -1;
- Gerrit._allPluginsPromise = undefined;
sandbox.restore();
});
@@ -132,12 +130,14 @@
});
test('lgtm plugin', done => {
+ Gerrit._resetPlugins();
const pluginHost = fixture('plugin-host');
pluginHost.config = {
plugin: {
js_resource_paths: [],
html_resource_paths: [
- new URL('test/plugin.html', window.location.href).toString(),
+ new URL('test/plugin.html?' + Math.random(),
+ window.location.href).toString(),
],
},
};
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 8225dac..1e37725 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -53,7 +53,7 @@
max-height: 100%;
}
section {
- border-top: 1px solid #cdcdcd;
+ border-top: 1px solid var(--border-color);
flex-shrink: 0;
padding: .5em 1.5em;
width: 100%;
@@ -82,7 +82,7 @@
padding-top: .1em;
}
.peopleListLabel {
- color: #666;
+ color: var(--deemphasized-text-color);
margin-top: .2em;
min-width: 7em;
padding-right: .5em;
@@ -120,7 +120,7 @@
display: block;
}
.previewContainer gr-formatted-text {
- background: #f6f6f6;
+ background: var(--header-background-color);
padding: 1em;
}
.draftsContainer h3 {
@@ -131,12 +131,12 @@
margin-left: 1em;
}
#checkingStatusLabel {
- color: #444;
+ color: var(--deemphasized-text-color);
font-style: italic;
}
#notLatestLabel,
#savingLabel {
- color: red;
+ color: var(--error-text-color);
}
#savingLabel {
display: none;
@@ -145,7 +145,7 @@
display: inline;
}
#pluginMessage {
- color: #444;
+ color: var(--deemphasized-text-color);
margin-left: 1em;
margin-bottom: .5em;
}
@@ -267,7 +267,7 @@
<template is="dom-if" if="[[canBeStarted]]">
<gr-button
link
- tertiary
+ secondary
disabled="[[_isState(knownLatestState, 'not-latest')]]"
class="action save"
has-tooltip
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/test/plugin.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/test/plugin.html
index 3a8b2e1..94787e6 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/test/plugin.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/test/plugin.html
@@ -28,6 +28,6 @@
replyApi.setLabelValue(label, '+1');
}
});
- }, '0.1', 'http://test.com/plugins/testplugin/static/test.js');
+ });
</script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index aa3ddea..ab1f55e 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -126,10 +126,16 @@
if (!change.labels[label].all) { return NaN; }
const detailed = change.labels[label].all.filter(
({_account_id}) => reviewer._account_id === _account_id).pop();
- if (!detailed || !detailed.hasOwnProperty('permitted_voting_range')) {
+ if (!detailed) {
return NaN;
}
- return detailed.permitted_voting_range.max;
+ if (detailed.hasOwnProperty('permitted_voting_range')) {
+ return detailed.permitted_voting_range.max;
+ } else if (detailed.hasOwnProperty('value')) {
+ // If preset, user can vote on the label.
+ return 0;
+ }
+ return NaN;
},
_computeReviewerTooltip(reviewer, change) {
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index 4e085d0..1a406c9 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -206,7 +206,6 @@
CC: reviewers,
},
};
- flushAsynchronousOperations();
assert.equal(element._hiddenReviewerCount, 0);
assert.equal(element._displayedReviewers.length, 6);
assert.equal(element._reviewers.length, 6);
@@ -230,7 +229,6 @@
CC: reviewers,
},
};
- flushAsynchronousOperations();
assert.equal(element._hiddenReviewerCount, 2);
assert.equal(element._displayedReviewers.length, 5);
assert.equal(element._reviewers.length, 7);
@@ -254,7 +252,6 @@
CC: reviewers,
},
};
- flushAsynchronousOperations();
assert.equal(element._hiddenReviewerCount, 0);
assert.equal(element._displayedReviewers.length, 7);
assert.equal(element._reviewers.length, 7);
@@ -278,14 +275,13 @@
CC: reviewers,
},
};
- flushAsynchronousOperations();
assert.equal(element._hiddenReviewerCount, 95);
assert.equal(element._displayedReviewers.length, 5);
assert.equal(element._reviewers.length, 100);
assert.isFalse(element.$$('.hiddenReviewers').hidden);
MockInteractions.tap(element.$$('.hiddenReviewers'));
- flushAsynchronousOperations();
+
assert.equal(element._hiddenReviewerCount, 0);
assert.equal(element._displayedReviewers.length, 100);
assert.equal(element._reviewers.length, 100);
@@ -303,7 +299,7 @@
{_account_id: 7, permitted_voting_range: {max: 1}}],
},
FooBar: {
- all: [{_account_id: 7, permitted_voting_range: {max: 0}}],
+ all: [{_account_id: 7, value: 0}],
},
},
permitted_labels: {
@@ -324,23 +320,13 @@
test('fails gracefully when all is not included', () => {
const change = {
- labels: {
- Foo: {},
- Bar: {},
- FooBar: {},
- },
+ labels: {Foo: {}},
permitted_labels: {
Foo: ['-1', ' 0', '+1', '+2'],
- Bar: ['-1', ' 0', '+1', '+2'],
- FooBar: ['-1', ' 0'],
},
};
assert.strictEqual(
element._computeReviewerTooltip({_account_id: 1}, change), '');
- assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 7}, change), '');
- assert.strictEqual(
- element._computeReviewerTooltip({_account_id: 2}, change), '');
});
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
index c3e65ab..1ca678932 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
@@ -35,9 +35,9 @@
}
.header {
align-items: center;
- background-color: #fafafa;
- border-bottom: 1px solid #ddd;
- border-top: 1px solid #ddd;
+ background-color: var(--table-header-background-color);
+ border-bottom: 1px solid var(--border-color);
+ border-top: 1px solid var(--border-color);
display: flex;
justify-content: left;
min-height: 3.2em;
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
index 9a5fea8..bbe2877 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -27,6 +27,12 @@
<style include="shared-styles">
gr-dropdown {
padding: .5em;
+ --gr-button: {
+ color: var(--header-text-color);
+ }
+ --gr-dropdown-item: {
+ color: var(--header-text-color);
+ }
}
gr-avatar {
height: 2em;
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index e2ef7c1..0b78df5 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -36,7 +36,7 @@
}
header {
align-items: center;
- border-bottom: 1px solid #cdcdcd;
+ border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
}
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 7af5fd5..fa0fe52 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -18,8 +18,10 @@
<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html">
<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html">
<link rel="import" href="../gr-smart-search/gr-smart-search.html">
@@ -35,7 +37,7 @@
display: flex;
}
.bigTitle {
- color: var(--primary-text-color);
+ color: var(--header-text-color);
font-size: 1.75rem;
text-decoration: none;
}
@@ -68,7 +70,7 @@
position: relative;
}
.linksTitle {
- color: var(--primary-text-color);
+ color: var(--header-text-color);
display: inline-block;
font-family: var(--font-family-bold);
position: relative;
@@ -95,7 +97,13 @@
.browse {
padding: .6em .5em;
}
+ gr-dropdown {
+ --gr-dropdown-item: {
+ color: var(--header-text-color);
+ }
+ }
.browse {
+ color: var(--header-text-color);
/* Same as gr-button */
margin: 5px 4px;
text-decoration: none;
@@ -115,13 +123,14 @@
text-overflow: ellipsis;
}
.loginButton {
+ color: var(--header-text-color);
padding: 1em;
}
.dropdown-trigger {
text-decoration: none;
}
.dropdown-content {
- background-color: #fff;
+ background-color: var(--view-background-color);
box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
}
@media screen and (max-width: 50em) {
@@ -163,15 +172,11 @@
</gr-dropdown>
</li>
</template>
- <li>
- <a
- class="browse linksTitle"
- href$="[[_computeRelativeURL('/admin/repos')]]">
- Browse</a>
- </li>
</ul>
<div class="rightItems">
- <gr-smart-search id="search" value="{{searchQuery}}"></gr-smart-search>
+ <gr-smart-search
+ id="search"
+ search-query="{{searchQuery}}"></gr-smart-search>
<gr-endpoint-decorator
class="hideOnMobile"
name="header-browse-source"></gr-endpoint-decorator>
@@ -181,6 +186,7 @@
</div>
</div>
</nav>
+ <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-main-header.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index 3ef6126..42c744f 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -93,7 +93,8 @@
},
_links: {
type: Array,
- computed: '_computeLinks(_defaultLinks, _userLinks, _docBaseUrl)',
+ computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' +
+ '_docBaseUrl)',
},
_loginURL: {
type: String,
@@ -106,6 +107,7 @@
},
behaviors: [
+ Gerrit.AdminNavBehavior,
Gerrit.BaseUrlBehavior,
Gerrit.DocsUrlBehavior,
],
@@ -149,7 +151,7 @@
return '//' + window.location.host + this.getBaseUrl() + path;
},
- _computeLinks(defaultLinks, userLinks, docBaseUrl) {
+ _computeLinks(defaultLinks, userLinks, adminLinks, docBaseUrl) {
const links = defaultLinks.slice();
if (userLinks && userLinks.length > 0) {
links.push({
@@ -165,6 +167,10 @@
class: 'hideOnMobile',
});
}
+ links.push({
+ title: 'Browse',
+ links: adminLinks,
+ });
return links;
},
@@ -186,10 +192,23 @@
},
_loadAccount() {
- return this.$.restAPI.getAccount().then(account => {
+ const promises = [
+ this.$.restAPI.getAccount(),
+ Gerrit.awaitPluginsLoaded(),
+ ];
+
+ return Promise.all(promises).then(result => {
+ const account = result[0];
this._account = account;
this.$.accountContainer.classList.toggle('loggedIn', account != null);
this.$.accountContainer.classList.toggle('loggedOut', account == null);
+
+ return this.getAdminLinks(account,
+ this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI),
+ this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI))
+ .then(res => {
+ this._adminLinks = res.links;
+ });
});
},
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
index 1fd0402..5d51546 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -87,15 +87,28 @@
name: 'Facebook',
url: 'https://facebook.com',
}];
+ const adminLinks = [{
+ name: 'Repos',
+ url: '/repos',
+ }];
// When no admin links are passed, it should use the default.
- assert.deepEqual(element._computeLinks(defaultLinks, []), defaultLinks);
- assert.deepEqual(
- element._computeLinks(defaultLinks, userLinks),
+ assert.deepEqual(element._computeLinks(defaultLinks, [], adminLinks),
defaultLinks.concat({
- title: 'Your',
- links: userLinks,
+ title: 'Browse',
+ links: adminLinks,
}));
+ assert.deepEqual(
+ element._computeLinks(defaultLinks, userLinks, adminLinks),
+ defaultLinks.concat([
+ {
+ title: 'Your',
+ links: userLinks,
+ },
+ {
+ title: 'Browse',
+ links: adminLinks,
+ }]));
});
test('documentation links', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 8ab5adf3..73a29a7 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -38,6 +38,25 @@
CATEGORY: 'exception',
};
+ const TIMER = {
+ CHANGE_DISPLAYED: 'ChangeDisplayed',
+ DASHBOARD_DISPLAYED: 'DashboardDisplayed',
+ DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed',
+ PLUGINS_LOADED: 'PluginsLoaded',
+ STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed',
+ STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed',
+ STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed',
+ WEB_COMPONENTS_READY: 'WebComponentsReady',
+ };
+
+ const STARTUP_TIMERS = {};
+ STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0;
+ STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0;
+ STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0;
+ STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0;
+ // WebComponentsReady timer is triggered from gr-router.
+ STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0;
+
const INTERACTION_TYPE = 'interaction';
const pending = [];
@@ -81,8 +100,8 @@
category: String,
_baselines: {
- type: Array,
- value() { return {}; },
+ type: Object,
+ value: STARTUP_TIMERS, // Shared across all instances.
},
},
@@ -91,7 +110,7 @@
},
now() {
- return Math.round(10 * window.performance.now()) / 10;
+ return window.performance.now();
},
reporter(...args) {
@@ -157,13 +176,46 @@
}
},
+ beforeLocationChanged() {
+ for (const prop of Object.keys(this._baselines)) {
+ delete this._baselines[prop];
+ }
+ this.time(TIMER.CHANGE_DISPLAYED);
+ this.time(TIMER.DASHBOARD_DISPLAYED);
+ this.time(TIMER.DIFF_VIEW_DISPLAYED);
+ },
+
locationChanged(page) {
this.reporter(
NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page);
},
+ dashboardDisplayed() {
+ if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) {
+ this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED);
+ } else {
+ this.timeEnd(TIMER.DASHBOARD_DISPLAYED);
+ }
+ },
+
+ changeDisplayed() {
+ if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) {
+ this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED);
+ } else {
+ this.timeEnd(TIMER.CHANGE_DISPLAYED);
+ }
+ },
+
+ diffViewDisplayed() {
+ if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) {
+ this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED);
+ } else {
+ this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED);
+ }
+ },
+
pluginsLoaded() {
- this.timeEnd('PluginsLoaded');
+ this.timeEnd(TIMER.PLUGINS_LOADED);
},
/**
@@ -177,8 +229,9 @@
* Finish named timer and report it to server.
*/
timeEnd(name) {
- const baseTime = this._baselines[name] || 0;
- const time = Math.round(this.now() - baseTime) + 'ms';
+ if (!this._baselines.hasOwnProperty(name)) { return; }
+ const baseTime = this._baselines[name];
+ const time = Math.round(this.now() - baseTime);
this.reporter(TIMING.TYPE, TIMING.CATEGORY, name, time);
delete this._baselines[name];
},
@@ -191,4 +244,5 @@
window.GrReporting = GrReporting;
// Expose onerror installation so it would be accessible from tests.
window.GrReporting._catchErrors = catchErrors;
+ window.GrReporting.STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS);
})();
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index 62ef2d6..3965c7d 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -45,6 +45,7 @@
sandbox = sinon.sandbox.create();
clock = sinon.useFakeTimers(NOW_TIME);
element = fixture('basic');
+ element._baselines = Object.assign({}, GrReporting.STARTUP_TIMERS);
fakePerformance = {
navigationStart: 1,
loadEventEnd: 2,
@@ -53,6 +54,7 @@
{get() { return fakePerformance; }});
sandbox.stub(element, 'reporter');
});
+
teardown(() => {
sandbox.restore();
clock.restore();
@@ -67,6 +69,14 @@
));
});
+ test('WebComponentsReady', () => {
+ sandbox.stub(element, 'now').returns(42);
+ element.timeEnd('WebComponentsReady');
+ assert.isTrue(element.reporter.calledWithExactly(
+ 'timing-report', 'UI Latency', 'WebComponentsReady', 42
+ ));
+ });
+
test('pageLoaded', () => {
element.pageLoaded();
assert.isTrue(
@@ -76,20 +86,63 @@
);
});
+ test('beforeLocationChanged', () => {
+ element._baselines['garbage'] = 'monster';
+ sandbox.stub(element, 'time');
+ element.beforeLocationChanged();
+ assert.isTrue(element.time.calledWithExactly('DashboardDisplayed'));
+ assert.isTrue(element.time.calledWithExactly('ChangeDisplayed'));
+ assert.isTrue(element.time.calledWithExactly('DiffViewDisplayed'));
+ assert.isFalse(element._baselines.hasOwnProperty('garbage'));
+ });
+
+ test('changeDisplayed', () => {
+ sandbox.spy(element, 'timeEnd');
+ element.changeDisplayed();
+ assert.isFalse(
+ element.timeEnd.calledWithExactly('ChangeDisplayed'));
+ assert.isTrue(
+ element.timeEnd.calledWithExactly('StartupChangeDisplayed'));
+ element.changeDisplayed();
+ assert.isTrue(element.timeEnd.calledWithExactly('ChangeDisplayed'));
+ });
+
+ test('diffViewDisplayed', () => {
+ sandbox.spy(element, 'timeEnd');
+ element.diffViewDisplayed();
+ assert.isFalse(
+ element.timeEnd.calledWithExactly('DiffViewDisplayed'));
+ assert.isTrue(
+ element.timeEnd.calledWithExactly('StartupDiffViewDisplayed'));
+ element.diffViewDisplayed();
+ assert.isTrue(element.timeEnd.calledWithExactly('DiffViewDisplayed'));
+ });
+
+ test('dashboardDisplayed', () => {
+ sandbox.spy(element, 'timeEnd');
+ element.dashboardDisplayed();
+ assert.isFalse(
+ element.timeEnd.calledWithExactly('DashboardDisplayed'));
+ assert.isTrue(
+ element.timeEnd.calledWithExactly('StartupDashboardDisplayed'));
+ element.dashboardDisplayed();
+ assert.isTrue(element.timeEnd.calledWithExactly('DashboardDisplayed'));
+ });
+
test('time and timeEnd', () => {
const nowStub = sandbox.stub(element, 'now').returns(0);
element.time('foo');
- nowStub.returns(1);
+ nowStub.returns(1.1);
element.time('bar');
nowStub.returns(2);
element.timeEnd('bar');
- nowStub.returns(3.123);
+ nowStub.returns(3.511);
element.timeEnd('foo');
assert.isTrue(element.reporter.calledWithExactly(
- 'timing-report', 'UI Latency', 'foo', '3ms'
+ 'timing-report', 'UI Latency', 'foo', 4
));
assert.isTrue(element.reporter.calledWithExactly(
- 'timing-report', 'UI Latency', 'bar', '1ms'
+ 'timing-report', 'UI Latency', 'bar', 1
));
});
@@ -105,7 +158,7 @@
sandbox.stub(element, 'now').returns(42);
element.pluginsLoaded();
assert.isTrue(element.defaultReporter.calledWithExactly(
- 'timing-report', 'UI Latency', 'PluginsLoaded', '42ms'
+ 'timing-report', 'UI Latency', 'PluginsLoaded', 42
));
});
@@ -117,11 +170,13 @@
test('reports if plugins are loaded', () => {
Gerrit._arePluginsLoaded.returns(true);
- element.timeEnd('foo');
+ element.pluginsLoaded();
assert.isTrue(element.defaultReporter.called);
});
test('reports cached events preserving order', () => {
+ element.time('foo');
+ element.time('bar');
Gerrit._arePluginsLoaded.returns(false);
element.timeEnd('foo');
Gerrit._arePluginsLoaded.returns(true);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index f6079a5..8af7301 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -182,9 +182,9 @@
(function() {
const reporting = document.createElement('gr-reporting');
- document.onload = function() {
+ window.addEventListener('load', () => {
reporting.pageLoaded();
- };
+ });
window.addEventListener('WebComponentsReady', () => {
reporting.timeEnd('WebComponentsReady');
@@ -199,6 +199,7 @@
type: Object,
value: app,
},
+ _isRedirecting: Boolean,
},
behaviors: [
@@ -217,6 +218,7 @@
},
_redirect(url) {
+ this._isRedirecting = true;
page.redirect(url);
},
@@ -640,13 +642,24 @@
return;
}
page(pattern, this._loadUserMiddleware.bind(this), data => {
- this.$.reporting.locationChanged(handlerName);
+ this.$.reporting.locationChanged(this._getPageName(handlerName, data));
const promise = opt_authRedirect ?
this._redirectIfNotLoggedIn(data) : Promise.resolve();
promise.then(() => { this[handlerName](data); });
});
},
+ _getPageName(handlerName, ctx) {
+ switch (handlerName) {
+ case '_handleChangeOrDiffRoute': {
+ const isDiffView = ctx.params[8];
+ return isDiffView ? Gerrit.Nav.View.DIFF : Gerrit.Nav.View.CHANGE;
+ }
+ default:
+ return handlerName;
+ }
+ },
+
_startRouter() {
const base = this.getBaseUrl();
if (base) {
@@ -659,6 +672,14 @@
params => this._generateWeblinks(params)
);
+ page.exit('*', (ctx, next) => {
+ if (!this._isRedirecting) {
+ this.$.reporting.beforeLocationChanged();
+ }
+ this._isRedirecting = false;
+ next();
+ });
+
// Middleware
page((ctx, next) => {
document.body.scrollTop = 0;
@@ -1316,8 +1337,6 @@
this._redirect(this._generateUrl(params));
} else {
this._setParams(params);
- this.$.restAPI.setInProjectLookup(params.changeNum,
- params.project);
}
},
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
index 3572e89..e0a7e46 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -1320,6 +1320,12 @@
assert.isTrue(normalizeRangeStub.called);
});
+ test('gr-reporting recognizes change page', () => {
+ const ctx = makeParams(null, '');
+ assert.equal(element._getPageName('_handleChangeOrDiffRoute', ctx),
+ Gerrit.Nav.View.CHANGE);
+ });
+
test('diff view', () => {
normalizeRangeStub.returns(false);
sandbox.stub(element, '_generateUrl').returns('foo');
@@ -1337,6 +1343,12 @@
assert.isFalse(redirectStub.called);
assert.isTrue(normalizeRangeStub.called);
});
+
+ test('gr-reporting recognizes diff page', () => {
+ const ctx = makeParams('foo/bar/baz', 'b44');
+ assert.equal(element._getPageName('_handleChangeOrDiffRoute', ctx),
+ Gerrit.Nav.View.DIFF);
+ });
});
test('_handleDiffEditRoute', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index b7b2147..39b2f7e 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -28,8 +28,8 @@
display: flex;
}
gr-autocomplete {
- background-color: white;
- border: 1px solid #d1d2d3;
+ background-color: var(--view-background-color);
+ border: 1px solid var(--border-color);
border-radius: 2px 0 0 2px;
flex: 1;
font: inherit;
diff --git a/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html b/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
index 1a6245d..22fd2aa 100644
--- a/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
+++ b/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
@@ -46,7 +46,7 @@
width: 73ch; /* Add a char to account for the border. */
--iron-autogrow-textarea {
- border: 1px solid #cdcdcd;
+ border: 1px solid var(--border-color);
box-sizing: border-box;
font-family: var(--monospace-font-family);
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html
index bdc412e..fb801e5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html
@@ -24,6 +24,7 @@
<style include="shared-styles">
:host {
display: block;
+ max-width: var(--content-width, 80ch);
white-space: normal;
}
gr-diff-comment-thread + gr-diff-comment-thread {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
index 6f597d7..de12ea4 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -28,7 +28,6 @@
<style include="shared-styles">
gr-button {
margin-left: .5em;
- --gr-button-color: #212121;
}
#actions {
margin-left: auto;
@@ -94,21 +93,25 @@
<gr-button
id="replyBtn"
link
+ secondary
class="action reply"
on-tap="_handleCommentReply">Reply</gr-button>
<gr-button
id="quoteBtn"
link
+ secondary
class="action quote"
on-tap="_handleCommentQuote">Quote</gr-button>
<gr-button
id="ackBtn"
link
+ secondary
class="action ack"
on-tap="_handleCommentAck">Ack</gr-button>
<gr-button
id="doneBtn"
link
+ secondary
class="action done"
on-tap="_handleCommentDone">Done</gr-button>
</div>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index f1bce4c..75a67bf 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -65,7 +65,7 @@
margin: 0;
}
.headerMiddle {
- color: #666;
+ color: var(--deemphasized-text-color);
flex: 1;
overflow: hidden;
}
@@ -88,7 +88,7 @@
}
a.date:link,
a.date:visited {
- color: #666;
+ color: var(--deemphasized-text-color);
}
.actions {
display: flex;
@@ -97,7 +97,6 @@
}
.action {
margin-left: 1em;
- --gr-button-color: #212121;
}
.robotActions {
display: flex;
@@ -169,7 +168,7 @@
display: none;
}
label.show-hide {
- color: #000;
+ color: var(--primary-text-color);
cursor: pointer;
display: block;
font-size: .8rem;
@@ -214,7 +213,7 @@
#deleteBtn {
display: none;
--gr-button: {
- color: #666;
+ color: var(--deemphasized-text-color);
padding: 0;
}
}
@@ -249,6 +248,7 @@
<gr-button
id="deleteBtn"
link
+ secondary
class$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]"
on-tap="_handleCommentDelete">
(Delete)
@@ -314,21 +314,35 @@
</label>
</div>
<div class="rightActions">
- <gr-button link class="action cancel hideOnPublished"
+ <gr-button
+ link
+ secondary
+ class="action cancel hideOnPublished"
on-tap="_handleCancel">Cancel</gr-button>
- <gr-button link class="action discard hideOnPublished"
+ <gr-button
+ link
+ secondary
+ class="action discard hideOnPublished"
on-tap="_handleDiscard">Discard</gr-button>
- <gr-button link class="action edit hideOnPublished"
+ <gr-button
+ link
+ secondary
+ class="action edit hideOnPublished"
on-tap="_handleEdit">Edit</gr-button>
- <gr-button link class="action save hideOnPublished"
- on-tap="_handleSave"
- disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]">Save
- </gr-button>
+ <gr-button
+ link
+ secondary
+ disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]"
+ class="action save hideOnPublished"
+ on-tap="_handleSave">Save</gr-button>
</div>
</div>
<div class="robotActions" hidden$="[[!_showRobotActions]]">
<template is="dom-if" if="[[isRobotComment]]">
- <gr-button link class="action fix"
+ <gr-button
+ link
+ secondary
+ class="action fix"
on-tap="_handleFix"
disabled="[[robotButtonDisabled]]">
Please Fix
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
index ae62b2e..c912a16 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
@@ -26,11 +26,11 @@
position: relative;
}
.contentWrapper ::content .range {
- background-color: rgba(255,213,0,0.5);
+ background-color: var(--diff-highlight-range-color);
display: inline;
}
.contentWrapper ::content .rangeHighlight {
- background-color: rgba(255,255,0,0.5);
+ background-color: var(--diff-highlight-range-hover-color);
display: inline;
}
gr-selection-action-box {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html
index be257d4..8251e53 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html
@@ -28,7 +28,7 @@
display: flex;
}
gr-button.selected iron-icon {
- color: var(--color-link);
+ color: var(--link-color);
}
iron-icon {
height: 1.3rem;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
index d4bcef4..68c4d39 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -45,7 +45,7 @@
padding: 1em 1.5em;
}
.header {
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color);
font-family: var(--font-family-bold);
}
.mainContainer {
@@ -65,7 +65,7 @@
flex: 1;
}
.actions {
- border-top: 1px solid #ddd;
+ border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index c7a1d86..1fc99b1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -23,6 +23,7 @@
<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html">
<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html">
@@ -53,12 +54,12 @@
gr-diff {
border: none;
--diff-container-styles: {
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid var(--border-color);
}
}
gr-fixed-panel {
- background-color: #fff;
- border-bottom: 1px #eee solid;
+ background-color: var(--view-background-color);
+ border-bottom: 1px solid var(--border-color);
z-index: 1;
}
header,
@@ -102,7 +103,7 @@
text-decoration: none;
}
.loading {
- color: #777;
+ color: var(--deemphasized-text-color);
font-size: 2rem;
height: 100%;
padding: 1em var(--default-horizontal-margin);
@@ -182,7 +183,7 @@
vertical-align: -.1em;
}
.mobileNavLink {
- color: #000;
+ color: var(--primary-text-color);
font-size: 1.5rem;
font-family: var(--font-family-bold);
text-decoration: none;
@@ -346,6 +347,7 @@
<gr-storage id="storage"></gr-storage>
<gr-diff-cursor id="cursor"></gr-diff-cursor>
<gr-comment-api id="commentAPI"></gr-comment-api>
+ <gr-reporting id="reporting"></gr-reporting>
</template>
<script src="gr-diff-view.js"></script>
</dom-module>
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 e37970a..5df640e 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
@@ -552,6 +552,10 @@
_paramsChanged(value) {
if (value.view !== Gerrit.Nav.View.DIFF) { return; }
+ if (value.changeNum && value.project) {
+ this.$.restAPI.setInProjectLookup(value.changeNum, value.project);
+ }
+
this.$.diff.lineOfInterest = this._getLineOfInterest(this.params);
this._initCursor(this.params);
@@ -614,7 +618,7 @@
promises.push(this._getChangeEdit(this._changeNum));
this._loading = true;
- Promise.all(promises).then(r => {
+ return Promise.all(promises).then(r => {
const edit = r[4];
if (edit) {
this.set('_change.revisions.' + edit.commit.commit, {
@@ -625,7 +629,9 @@
}
this._loading = false;
this.$.diff.comments = this._commentsForDiff;
- this.$.diff.reload();
+ return this.$.diff.reload();
+ }).then(() => {
+ this.$.reporting.diffViewDisplayed();
});
},
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 c85ea28..620286b 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,23 @@
sandbox.restore();
});
+ test('params change triggers diffViewDisplayed()', () => {
+ sandbox.stub(element.$.reporting, 'diffViewDisplayed');
+ sandbox.stub(element.$.diff, 'reload').returns(Promise.resolve());
+ sandbox.spy(element, '_paramsChanged');
+ element.params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ patchNum: '2',
+ basePatchNum: '1',
+ path: '/COMMIT_MSG',
+ };
+
+ return element._paramsChanged.returnValues[0].then(() => {
+ assert.isTrue(element.$.reporting.diffViewDisplayed.calledOnce);
+ });
+ });
+
test('toggle left diff with a hotkey', () => {
const toggleLeftDiffStub = sandbox.stub(element.$.diff, 'toggleLeftDiff');
MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
@@ -1041,5 +1058,19 @@
assert.isFalse(isVisible(element.$.reviewed));
});
});
+
+ test('_paramsChanged sets in projectLookup', () => {
+ sandbox.stub(element, '_getLineOfInterest');
+ sandbox.stub(element, '_initCursor');
+ const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup');
+ element._paramsChanged({
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: 101,
+ project: 'test-project',
+ path: '',
+ });
+ assert.isTrue(setStub.calledOnce);
+ assert.isTrue(setStub.calledWith(101, 'test-project'));
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
index fc23837..3972751 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -25,7 +25,7 @@
<link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html">
<link rel="import" href="../gr-diff-highlight/gr-diff-highlight.html">
<link rel="import" href="../gr-diff-selection/gr-diff-selection.html">
-<link rel="import" href="../gr-syntax-themes/gr-theme-default.html">
+<link rel="import" href="../gr-syntax-themes/gr-syntax-theme.html">
<script src="../../../scripts/hiddenscroll.js"></script>
@@ -58,11 +58,11 @@
}
table {
border-collapse: collapse;
- border-right: 1px solid #ddd;
+ border-right: 1px solid var(--border-color);
table-layout: fixed;
}
.lineNum {
- background-color: #eee;
+ background-color: var(--header-background-color);
}
.image-diff .gr-diff {
text-align: center;
@@ -83,11 +83,11 @@
.diff-row.target-row.target-side-right .lineNum.right,
.diff-row.target-row.unified .lineNum {
background-color: #BBDEFB;
- color: #000;
+ color: var(--primary-text-color);
}
.blank,
.content {
- background-color: #fff;
+ background-color: var(--view-background-color);
}
.full-width {
width: 100%;
@@ -110,7 +110,7 @@
-ms-user-select: none;
user-select: none;
- color: #666;
+ color: var(--deemphasized-text-color);
padding: 0 .5em;
text-align: right;
}
@@ -119,9 +119,9 @@
}
.content {
overflow: hidden;
- /* Set max and min width since setting width on table cells still
- allows them to shrink. */
- max-width: var(--content-width, 80ch);
+ /* Set min width since setting width on table cells still
+ allows them to shrink. Do not set max width because
+ CJK (Chinese-Japanese-Korean) glyphs have variable width */
min-width: var(--content-width, 80ch);
width: var(--content-width, 80ch);
}
@@ -164,9 +164,7 @@
.contextControl gr-button {
display: inline-block;
text-decoration: none;
- --gr-button-color: rgba(0,0,0,.54);
--gr-button: {
- font-family: var(--monospace-font-family);
padding: .2em;
}
}
@@ -212,7 +210,7 @@
display: block;
}
.target-row td.blame {
- background: #eee;
+ background: var(--header-background-color);
}
col.blame {
display: none;
@@ -257,7 +255,7 @@
background-repeat: repeat-y;
}
</style>
- <style include="gr-theme-default"></style>
+ <style include="gr-syntax-theme"></style>
<div id="diffHeader" hidden$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]">
<template
is="dom-repeat"
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
index cfc76b7..3de4284 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -33,12 +33,12 @@
max-width: 15em;
}
.arrow {
- color: rgba(0,0,0,.7);
+ color: var(--deemphasized-text-color);
margin: 0 .5em;
}
gr-dropdown-list {
--trigger-style: {
- color: rgba(0,0,0,.7);
+ color: var(--deemphasized-text-color);
text-transform: none;
font-family: var(--font-family);
}
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
new file mode 100644
index 0000000..3dfbf34
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.html
@@ -0,0 +1,104 @@
+<!--
+@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.
+-->
+<dom-module id="gr-syntax-theme">
+ <template>
+ <style>
+ /**
+ * @overview Highlight.js emits the following classes that do not have
+ * styles here:
+ * subst, symbol, class, function, doctag, meta-string, section, name,
+ * builtin-name, bulletm, code, formula, quote, addition, deletion,
+ * attribute
+ * @see {@link http://highlightjs.readthedocs.io/en/latest/css-classes-reference.html}
+ */
+
+ .contentText {
+ color: var(--syntax-default-color);
+ }
+ .gr-syntax-meta {
+ color: var(--syntax-meta-color);
+ }
+ .gr-syntax-keyword {
+ color: var(--syntax-keyword-color);
+ line-height: 1;
+ }
+ .gr-syntax-number {
+ color: var(--syntax-number-color);
+ }
+ .gr-syntax-selector-class {
+ color: var(--syntax-selector-class-color);
+ }
+ .gr-syntax-variable {
+ color: var(--syntax-variable-color);
+ }
+ .gr-syntax-template-variable {
+ color: var(--syntax-template-variable-color);
+ }
+ .gr-syntax-comment {
+ color: var(--syntax-comment-color);
+ }
+ .gr-syntax-string {
+ color: var(--syntax-string-color);
+ }
+ .gr-syntax-selector-id {
+ color: var(--syntax-selector-id-color);
+ }
+ .gr-syntax-built_in {
+ color: var(--syntax-built_in-color);
+ }
+ .gr-syntax-tag {
+ color: var(--syntax-tag-color);
+ }
+ .gr-syntax-link {
+ color: var(--syntax-link-color);
+ }
+ .gr-syntax-meta-keyword {
+ color: var(--syntax-meta-keyword-color);
+ }
+ .gr-syntax-type {
+ color: var(--syntax-type-color);
+ }
+ .gr-syntax-title {
+ color: var(--syntax-title-color);
+ }
+ .gr-syntax-attr {
+ color: var(--syntax-attr-color);
+ }
+ .gr-syntax-literal { /* XML/HTML Attribute */
+ color: var(--syntax-literal-color);
+ }
+ .gr-syntax-selector-pseudo {
+ color: var(--syntax-selector-pseudo-color);
+ }
+ .gr-syntax-regexp {
+ color: var(--syntax-regexp-color);
+ }
+ .gr-syntax-selector-attr {
+ color: var(--syntax-selector-attr-color);
+ }
+ .gr-syntax-template-tag {
+ color: var(--syntax-template-tag-color);
+ }
+ .gr-syntax-emphasis {
+ font-style: italic;
+ }
+ .gr-syntax-strong {
+ font-weight: 700;
+ }
+ </style>
+ </template>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
deleted file mode 100644
index b6a84ad..0000000
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
+++ /dev/null
@@ -1,87 +0,0 @@
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<dom-module id="gr-theme-default">
- <template>
- <style>
- /**
- * @overview Highlight.js emits the following classes that do not have
- * styles here:
- * subst, symbol, class, function, doctag, meta-string, section, name,
- * builtin-name, bulletm, code, formula, quote, addition, deletion,
- * attribute
- * @see {@link http://highlightjs.readthedocs.io/en/latest/css-classes-reference.html}
- */
-
- .gr-syntax-meta {
- color: #FF1717;
- }
- .gr-syntax-keyword {
- color: #9E0069;
- line-height: 1;
- }
- .gr-syntax-number,
- .gr-syntax-selector-class {
- color: #164;
- }
- .gr-syntax-variable {
- color: black;
- }
- .gr-syntax-template-variable {
- color: #0000C0;
- }
- .gr-syntax-comment {
- color: #3F7F5F;
- }
- .gr-syntax-string,
- .gr-syntax-selector-id {
- color: #2A00FF;
- }
- .gr-syntax-built_in {
- color: #30a;
- }
- .gr-syntax-tag {
- color: #170;
- }
- .gr-syntax-link,
- .gr-syntax-meta-keyword {
- color: #219;
- }
- .gr-syntax-type {
- color: var(--color-link);
- }
- .gr-syntax-title {
- color: #0000C0;
- }
- .gr-syntax-attr,
- .gr-syntax-literal { /* XML/HTML Attribute */
- color: #219;
- }
- .gr-syntax-selector-pseudo,
- .gr-syntax-regexp,
- .gr-syntax-selector-attr,
- .gr-syntax-template-tag {
- color: #FA8602;
- }
- .gr-syntax-emphasis {
- font-style: italic;
- }
- .gr-syntax-strong {
- font-weight: 700;
- }
- </style>
- </template>
-</dom-module>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
index 2226383..61a9e69 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.html
@@ -54,7 +54,7 @@
}
gr-autocomplete {
--gr-autocomplete: {
- border: 1px solid #d1d2d3;
+ border: 1px solid var(--border-color);
border-radius: 2px;
font-size: var(--font-size-normal);
height: 2em;
@@ -62,7 +62,7 @@
}
}
input {
- border: 1px solid #d1d2d3;
+ border: 1px solid var(--border-color);
border-radius: 2px;
font-size: var(--font-size-normal);
height: 2em;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
index fd6aeb0..c57a147 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.html
@@ -44,7 +44,7 @@
--gr-dropdown-item: {
background-color: transparent;
border: none;
- color: var(--color-link);
+ color: var(--link-color);
font-size: inherit;
text-transform: uppercase;
}
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
index 8939a4f..6597f4a 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.html
@@ -27,6 +27,7 @@
<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
<link rel="import" href="../../shared/gr-fixed-panel/gr-fixed-panel.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../shared/gr-storage/gr-storage.html">
<link rel="import" href="../gr-default-editor/gr-default-editor.html">
<link rel="import" href="../../../styles/shared-styles.html">
@@ -38,7 +39,7 @@
}
gr-fixed-panel {
background-color: #ebf5fb;
- border-bottom: 1px #ddd solid;
+ border-bottom: 1px var(--border-color) solid;
z-index: 1;
}
header,
@@ -61,7 +62,7 @@
}
}
.textareaWrapper {
- border: 1px solid #ddd;
+ border: 1px solid var(--border-color);
border-radius: 3px;
margin: var(--default-horizontal-margin);
}
@@ -120,6 +121,7 @@
</gr-endpoint-decorator>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-storage id="storage"></gr-storage>
</template>
<script src="gr-editor-view.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index f2d1d76..46d5180 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -17,10 +17,13 @@
(function() {
'use strict';
+ const RESTORED_MESSAGE = 'Content restored from a previous edit.';
const SAVING_MESSAGE = 'Saving changes...';
const SAVED_MESSAGE = 'All changes saved';
const SAVE_FAILED_MSG = 'Failed to save changes';
+ const STORAGE_DEBOUNCE_INTERVAL_MS = 100;
+
Polymer({
is: 'gr-editor-view',
@@ -87,6 +90,10 @@
this._getEditPrefs().then(prefs => { this._prefs = prefs; });
},
+ get storageKey() {
+ return `c${this._changeNum}_ps${this._patchNum}_${this._path}`;
+ },
+
_getLoggedIn() {
return this.$.restAPI.getLoggedIn();
},
@@ -143,9 +150,19 @@
},
_getFileData(changeNum, path, patchNum) {
+ const storedContent =
+ this.$.storage.getEditableContentItem(this.storageKey);
+
return this.$.restAPI.getFileContent(changeNum, path, patchNum)
.then(res => {
- this._newContent = res.content || '';
+ if (storedContent && storedContent.message) {
+ this.dispatchEvent(new CustomEvent('show-alert',
+ {detail: {message: RESTORED_MESSAGE}, bubbles: true}));
+
+ this._newContent = storedContent.message;
+ } else {
+ this._newContent = res.content || '';
+ }
this._content = res.content || '';
// A non-ok response may result if the file does not yet exist.
@@ -162,6 +179,7 @@
_saveEdit() {
this._saving = true;
this._showAlert(SAVING_MESSAGE);
+ this.$.storage.eraseEditableContentItem(this.storageKey);
return this.$.restAPI.saveChangeEdit(this._changeNum, this._path,
this._newContent).then(res => {
this._saving = false;
@@ -191,7 +209,15 @@
},
_handleContentChange(e) {
- if (e.detail.value) { this.set('_newContent', e.detail.value); }
+ this.debounce('store', () => {
+ const content = e.detail.value;
+ if (content) {
+ this.set('_newContent', e.detail.value);
+ this.$.storage.setEditableContentItem(this.storageKey, content);
+ } else {
+ this.$.storage.eraseEditableContentItem(this.storageKey);
+ }
+ }, STORAGE_DEBOUNCE_INTERVAL_MS);
},
_handleSaveShortcut(e) {
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
index 193c2ed..4cf25fe 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
@@ -118,14 +118,18 @@
});
test('reacts to content-change event', () => {
+ const storeStub = sandbox.spy(element.$.storage, 'setEditableContentItem');
element._newContent = 'test';
element.$.editorEndpoint.dispatchEvent(new CustomEvent('content-change', {
bubbles: true,
detail: {value: 'new content value'},
}));
+ element.flushDebouncer('store');
flushAsynchronousOperations();
assert.equal(element._newContent, 'new content value');
+ assert.isTrue(storeStub.called);
+ assert.equal(storeStub.lastCall.args[1], 'new content value');
});
suite('edit file content', () => {
@@ -147,6 +151,8 @@
test('file modification and save, !ok response', () => {
const saveSpy = sandbox.spy(element, '_saveEdit');
+ const eraseStub = sandbox.stub(element.$.storage,
+ 'eraseEditableContentItem');
const alertStub = sandbox.stub(element, '_showAlert');
saveFileStub.returns(Promise.resolve({ok: false}));
element._newContent = newText;
@@ -163,6 +169,7 @@
return saveSpy.lastCall.returnValue.then(() => {
assert.isTrue(saveFileStub.called);
+ assert.isTrue(eraseStub.called);
assert.isFalse(element._saving);
assert.equal(alertStub.lastCall.args[0], 'Failed to save changes');
assert.deepEqual(saveFileStub.lastCall.args,
@@ -219,6 +226,7 @@
element._newContent = 'initial';
element._content = 'initial';
element._type = 'initial';
+ sandbox.stub(element.$.storage, 'getEditableContentItem').returns(null);
});
test('res.ok', () => {
@@ -343,5 +351,37 @@
});
});
});
+
+ suite('gr-storage caching', () => {
+ test('local edit exists', () => {
+ sandbox.stub(element.$.storage, 'getEditableContentItem')
+ .returns({message: 'pending edit'});
+ sandbox.stub(element.$.restAPI, 'getFileContent')
+ .returns(Promise.resolve({
+ ok: true,
+ type: 'text/javascript',
+ content: 'old content',
+ }));
+
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+
+ return element._getFileData(1, 'test', 1).then(() => {
+ flushAsynchronousOperations();
+
+ assert.isTrue(alertStub.called);
+ assert.equal(element._newContent, 'pending edit');
+ assert.equal(element._content, 'old content');
+ assert.equal(element._type, 'text/javascript');
+ });
+ });
+
+ test('storage key computation', () => {
+ element._changeNum = 1;
+ element._patchNum = 1;
+ element._path = 'test';
+ assert.equal(element.storageKey, 'c1_ps1_test');
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 809ca22..7cfb3b0 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -64,9 +64,10 @@
<template>
<style include="shared-styles">
:host {
+ background-color: var(--view-background-color);
display: flex;
- min-height: 100%;
flex-direction: column;
+ min-height: 100%;
}
gr-fixed-panel {
/**
@@ -82,7 +83,7 @@
gr-main-header {
background-color: var(--header-background-color);
padding: 0 var(--default-horizontal-margin);
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color);
}
gr-main-header.shadow {
/* Make it obvious for shadow dom testing */
@@ -90,6 +91,7 @@
}
footer {
background-color: var(--footer-background-color);
+ border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
padding: .5rem var(--default-horizontal-margin);
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 3a45575..282dae2 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -98,6 +98,7 @@
observers: [
'_viewChanged(params.view)',
+ '_paramsChanged(params.*)',
],
behaviors: [
@@ -227,15 +228,12 @@
pathname += '@' + hash;
}
this.set('_path', pathname);
- this._handleSearchPageChange();
},
- _handleSearchPageChange() {
- if (!this.params) {
- return;
- }
+ _paramsChanged(paramsRecord) {
+ const params = paramsRecord.base;
const viewsToCheck = [Gerrit.Nav.View.SEARCH, Gerrit.Nav.View.DASHBOARD];
- if (viewsToCheck.includes(this.params.view)) {
+ if (viewsToCheck.includes(params.view)) {
this.set('_lastSearchPage', location.pathname);
}
},
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index f0900aa..fb1b241 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -55,6 +55,8 @@
});
},
getPreferences() { return Promise.resolve({my: []}); },
+ getDiffPreferences() { return Promise.resolve({}); },
+ getEditPreferences() { return Promise.resolve({}); },
getVersion() { return Promise.resolve(42); },
probePath() { return Promise.resolve(42); },
});
@@ -87,7 +89,6 @@
hash: '#2',
host: location.host,
};
- sandbox.stub(element, '_handleSearchPageChange');
element._handleLocationChange({detail: curLocation});
flush(() => {
@@ -107,6 +108,13 @@
});
});
+ test('_paramsChanged sets search page', () => {
+ element._paramsChanged({base: {view: Gerrit.Nav.View.CHANGE}});
+ assert.notOk(element._lastSearchPage);
+ element._paramsChanged({base: {view: Gerrit.Nav.View.SEARCH}});
+ assert.ok(element._lastSearchPage);
+ });
+
suite('_jumpKeyPressed', () => {
let navStub;
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html
new file mode 100644
index 0000000..eddb52b
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html
@@ -0,0 +1,22 @@
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-change-metadata-api">
+ <script src="gr-change-metadata-api.js"></script>
+</dom-module>
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
new file mode 100644
index 0000000..b550f73
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
@@ -0,0 +1,39 @@
+/**
+ * @license
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+(function(window) {
+ 'use strict';
+
+ function GrChangeMetadataApi(plugin) {
+ this._hook = null;
+ this.plugin = plugin;
+ }
+
+ GrChangeMetadataApi.prototype._createHook = function() {
+ this._hook = this.plugin.hook('change-metadata-item');
+ };
+
+ GrChangeMetadataApi.prototype.onLabelsChanged = function(callback) {
+ if (!this._hook) {
+ this._createHook();
+ }
+ this._hook.onAttached(element =>
+ this.plugin.attributeHelper(element).bind('labels', callback));
+ return this;
+ };
+
+ window.GrChangeMetadataApi = GrChangeMetadataApi;
+})(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index 982123c..a7cfca3 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -34,11 +34,13 @@
_configChanged(config) {
const plugins = config.plugin;
const htmlPlugins = plugins.html_resource_paths || [];
- const jsPlugins = this._handleMigrations(plugins.js_resource_paths || [],
- htmlPlugins);
+ const jsPlugins =
+ this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins);
const defaultTheme = config.default_theme;
- Gerrit._setPluginsCount(
- jsPlugins.length + htmlPlugins.length + (defaultTheme ? 1 : 0));
+ const pluginsPending =
+ [].concat(jsPlugins, htmlPlugins, defaultTheme || []).map(
+ p => this._urlFor(p));
+ Gerrit._setPluginsPending(pluginsPending);
if (defaultTheme) {
// Make theme first to be first to load.
// Load sync to work around rare theme loading race condition.
@@ -72,7 +74,9 @@
// onload (second param) needs to be a function. When null or undefined
// were passed, plugins were not loaded correctly.
this.importHref(
- this._urlFor(url), () => {}, Gerrit._pluginInstalled, async);
+ this._urlFor(url), () => {},
+ Gerrit._pluginInstallError.bind(null, `${url} import error`),
+ async);
}
},
@@ -86,18 +90,20 @@
const el = document.createElement('script');
el.defer = true;
el.src = url;
- el.onerror = Gerrit._pluginInstalled;
+ el.onerror = Gerrit._pluginInstallError.bind(null, `${url} load error`);
return document.body.appendChild(el);
},
_urlFor(pathOrUrl) {
if (pathOrUrl.startsWith('http')) {
+ // Plugins are loaded from another domain.
return pathOrUrl;
}
if (!pathOrUrl.startsWith('/')) {
pathOrUrl = '/' + pathOrUrl;
}
- return this.getBaseUrl() + pathOrUrl;
+ const {href, pathname} = window.location;
+ return href.split(pathname)[0] + this.getBaseUrl() + pathOrUrl;
},
});
})();
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index ac9e69c..d31825f 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -36,12 +36,14 @@
suite('gr-diff tests', () => {
let element;
let sandbox;
+ let url;
setup(() => {
element = fixture('basic');
sandbox = sinon.sandbox.create();
sandbox.stub(document.body, 'appendChild');
sandbox.stub(element, 'importHref');
+ url = window.location.href.split(window.location.pathname)[0];
});
teardown(() => {
@@ -60,36 +62,43 @@
});
test('imports relative html plugins from config', () => {
+ sandbox.stub(Gerrit, '_pluginInstallError');
element.config = {
plugin: {html_resource_paths: ['foo/bar', 'baz']},
};
- assert.equal(element.importHref.firstCall.args[0], '/foo/bar');
- assert.equal(element.importHref.firstCall.args[2],
- Gerrit._pluginInstalled);
+ assert.equal(element.importHref.firstCall.args[0], url + '/foo/bar');
assert.isTrue(element.importHref.firstCall.args[3]);
- assert.equal(element.importHref.secondCall.args[0], '/baz');
- assert.equal(element.importHref.secondCall.args[2],
- Gerrit._pluginInstalled);
+ assert.equal(element.importHref.secondCall.args[0], url + '/baz');
assert.isTrue(element.importHref.secondCall.args[3]);
+
+ assert.equal(Gerrit._pluginInstallError.callCount, 0);
+ element.importHref.firstCall.args[2]();
+ assert.equal(Gerrit._pluginInstallError.callCount, 1);
+ element.importHref.secondCall.args[2]();
+ assert.equal(Gerrit._pluginInstallError.callCount, 2);
});
test('imports relative html plugins from config with a base url', () => {
+ sandbox.stub(Gerrit, '_pluginInstallError');
sandbox.stub(element, 'getBaseUrl').returns('/the-base');
element.config = {
plugin: {html_resource_paths: ['foo/bar', 'baz']}};
- assert.equal(element.importHref.firstCall.args[0], '/the-base/foo/bar');
- assert.equal(element.importHref.firstCall.args[2],
- Gerrit._pluginInstalled);
+ assert.equal(element.importHref.firstCall.args[0],
+ url + '/the-base/foo/bar');
assert.isTrue(element.importHref.firstCall.args[3]);
- assert.equal(element.importHref.secondCall.args[0], '/the-base/baz');
- assert.equal(element.importHref.secondCall.args[2],
- Gerrit._pluginInstalled);
+ assert.equal(element.importHref.secondCall.args[0],
+ url + '/the-base/baz');
assert.isTrue(element.importHref.secondCall.args[3]);
+ assert.equal(Gerrit._pluginInstallError.callCount, 0);
+ element.importHref.firstCall.args[2]();
+ assert.equal(Gerrit._pluginInstallError.callCount, 1);
+ element.importHref.secondCall.args[2]();
+ assert.equal(Gerrit._pluginInstallError.callCount, 2);
});
- test('inportHref is not called with null callback functions', () => {
+ test('importHref is not called with null callback functions', () => {
const plugins = ['path/to/plugin'];
element._importHtmlPlugins(plugins);
assert.isTrue(element.importHref.calledOnce);
@@ -98,6 +107,7 @@
});
test('imports absolute html plugins from config', () => {
+ sandbox.stub(Gerrit, '_pluginInstallError');
element.config = {
plugin: {
html_resource_paths: [
@@ -108,15 +118,16 @@
};
assert.equal(element.importHref.firstCall.args[0],
'http://example.com/foo/bar');
- assert.equal(element.importHref.firstCall.args[2],
- Gerrit._pluginInstalled);
assert.isTrue(element.importHref.firstCall.args[3]);
assert.equal(element.importHref.secondCall.args[0],
'https://example.com/baz');
- assert.equal(element.importHref.secondCall.args[2],
- Gerrit._pluginInstalled);
assert.isTrue(element.importHref.secondCall.args[3]);
+ assert.equal(Gerrit._pluginInstallError.callCount, 0);
+ element.importHref.firstCall.args[2]();
+ assert.equal(Gerrit._pluginInstallError.callCount, 1);
+ element.importHref.secondCall.args[2]();
+ assert.equal(Gerrit._pluginInstallError.callCount, 2);
});
test('adds js plugins from config to the body', () => {
@@ -127,16 +138,18 @@
test('imports relative js plugins from config', () => {
sandbox.stub(element, '_createScriptTag');
element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
- assert.isTrue(element._createScriptTag.calledWith('/foo/bar'));
- assert.isTrue(element._createScriptTag.calledWith('/baz'));
+ assert.isTrue(element._createScriptTag.calledWith(url + '/foo/bar'));
+ assert.isTrue(element._createScriptTag.calledWith(url + '/baz'));
});
test('imports relative html plugins from config with a base url', () => {
sandbox.stub(element, '_createScriptTag');
sandbox.stub(element, 'getBaseUrl').returns('/the-base');
element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
- assert.isTrue(element._createScriptTag.calledWith('/the-base/foo/bar'));
- assert.isTrue(element._createScriptTag.calledWith('/the-base/baz'));
+ assert.isTrue(element._createScriptTag.calledWith(
+ url + '/the-base/foo/bar'));
+ assert.isTrue(element._createScriptTag.calledWith(
+ url + '/the-base/baz'));
});
test('imports absolute html plugins from config', () => {
@@ -156,21 +169,23 @@
});
test('default theme is loaded with html plugins', () => {
+ sandbox.stub(Gerrit, '_pluginInstallError');
element.config = {
default_theme: '/oof',
plugin: {
html_resource_paths: ['some'],
},
};
- assert.equal(element.importHref.firstCall.args[0], '/oof');
- assert.equal(element.importHref.firstCall.args[2],
- Gerrit._pluginInstalled);
+ assert.equal(element.importHref.firstCall.args[0], url + '/oof');
assert.isFalse(element.importHref.firstCall.args[3]);
- assert.equal(element.importHref.secondCall.args[0], '/some');
- assert.equal(element.importHref.secondCall.args[2],
- Gerrit._pluginInstalled);
+ assert.equal(element.importHref.secondCall.args[0], url + '/some');
assert.isTrue(element.importHref.secondCall.args[3]);
+ assert.equal(Gerrit._pluginInstallError.callCount, 0);
+ element.importHref.firstCall.args[2]();
+ assert.equal(Gerrit._pluginInstallError.callCount, 1);
+ element.importHref.secondCall.args[2]();
+ assert.equal(Gerrit._pluginInstallError.callCount, 2);
});
});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
index a15dfe4..8d23ea2 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
@@ -65,7 +65,7 @@
stub('gr-custom-plugin-header', {
ready() { customHeader = this; },
});
- Gerrit._resolveAllPluginsLoaded();
+ Gerrit._setPluginsPending([]);
});
test('sets logo and title', done => {
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
index 7e45abc..fa188d7 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
@@ -38,7 +38,7 @@
text-align: center;
}
.checkboxContainer:hover {
- outline: 1px solid #ddd;
+ outline: 1px solid var(--border-color);
}
</style>
<div class="gr-form-styles">
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
index 6ae8295..0a7433e 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
@@ -26,7 +26,7 @@
<style include="shared-styles"></style>
<style include="gr-form-styles">
th {
- color: #666;
+ color: var(--deemphasized-text-color);
text-align: left;
}
#emailTable .emailColumn {
@@ -46,7 +46,7 @@
height: auto;
}
.preferredControl:hover {
- outline: 1px solid #d1d2d3;
+ outline: 1px solid var(--border-color);
}
</style>
<div class="gr-form-styles">
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
index 4db12ec..79c8a3b 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
@@ -37,7 +37,7 @@
margin-bottom: 1em;
}
header {
- border-bottom: 1px solid #cdcdcd;
+ border-bottom: 1px solid var(--border-color);
font-family: var(--font-family-bold);
margin-bottom: 1em;
}
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index 894c894..48b01f6 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -43,6 +43,9 @@
<dom-module id="gr-settings-view">
<template>
<style include="shared-styles">
+ :host {
+ color: var(--primary-text-color);
+ }
#newEmailInput {
width: 20em;
}
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
index 558140f..52649db 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -34,10 +34,10 @@
text-align: center;
}
.notifControl:hover {
- outline: 1px solid #ddd;
+ outline: 1px solid var(--border-color);
}
.projectFilter {
- color: #777;
+ color: var(--deemphasized-text-color);
font-style: italic;
margin-left: 1em;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
index ebbc7f5..f04caaa 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
@@ -31,7 +31,7 @@
}
.container {
align-items: center;
- background: #eee;
+ background: var(--header-background-color);
border-radius: .75em;
display: inline-flex;
padding: 0 .5em;
@@ -48,7 +48,7 @@
gr-button.remove {
--gr-button: {
border: 0;
- color: #666;
+ color: var(--deemphasized-text-color);
font-size: 1.7rem;
font-weight: normal;
height: .6em;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
index fe77db2..7fb4cab 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -20,6 +20,7 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../gr-avatar/gr-avatar.html">
+<link rel="import" href="../gr-limited-text/gr-limited-text.html">
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-account-label">
@@ -63,7 +64,7 @@
[[_computeEmailStr(account)]]
</span>
<template is="dom-if" if="[[account.status]]">
- <span>([[account.status]])</span>
+ (<gr-limited-text limit="20" text="[[account.status]]"></gr-limited-text>)
</template>
</span>
</span>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
index bea8d27..34b0de6 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
@@ -28,7 +28,7 @@
display: inline-block;
}
a {
- color: var(--default-text-color);
+ color: var(--primary-text-color);
text-decoration: none;
}
gr-account-label {
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
index 0e6a000..b47d5a4 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
@@ -33,7 +33,7 @@
bottom: 1.25rem;
border-radius: 3px;
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
- color: #fff;
+ color: var(--view-background-color);
left: 1.25rem;
padding: 1em 1.5em;
position: fixed;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
index ef9ed4e..4e076a89 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
@@ -36,17 +36,24 @@
list-style: none;
}
li {
+ border-bottom: 1px solid var(--border-color);
cursor: pointer;
padding: .5em .75em;
}
+ li:last-of-type {
+ border: none;
+ }
li:focus {
outline: none;
}
+ li:hover {
+ background-color: var(--hover-background-color);
+ }
li.selected {
- background-color: #eee;
+ background-color: var(--selection-background-color);
}
.dropdown-content {
- background: #fff;
+ background: var(--dropdown-background-color);
box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
}
</style>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
index 971780e..c289aa3 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -35,7 +35,7 @@
margin: 0 .25em;
}
paper-input:not(.borderless) {
- border: 1px solid #ddd;
+ border: 1px solid var(--border-color);
}
paper-input {
height: 100%;
@@ -43,7 +43,6 @@
@apply --gr-autocomplete;
--paper-input-container: {
padding: 0;
- min-width: 15em;
}
--paper-input-container-input: {
font-size: var(--font-size-normal);
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
index 6dd197d..8fff850 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -27,8 +27,8 @@
<style include="shared-styles">
/* general styles for all buttons */
:host {
- --background-color: var(--gr-button-background, #fff);
- --button-color: var(--gr-button-color, var(--color-link));
+ --background-color: var(--button-background-color, var(--default-button-background-color));
+ --text-color: var(--default-button-text-color);
display: inline-block;
font-family: var(--font-family-bold);
font-size: var(--font-size-small);
@@ -46,7 +46,7 @@
-webkit-font-smoothing: initial;
align-items: center;
background-color: var(--background-color);
- color: var(--button-color);
+ color: var(--text-color);
display: flex;
font-family: inherit;
justify-content: center;
@@ -62,18 +62,26 @@
), var(--background-color);
}
- /* Styles for raised buttons specifically */
- :host([primary][raised]),
- :host([secondary][raised]) {
- --background-color: var(--color-link);
- --button-color: #fff;
+ :host([primary]) {
+ --background-color: var(--primary-button-background-color);
+ --text-color: var(--primary-button-text-color);
+ }
+ :host([link][primary]) {
+ --text-color: var(--primary-button-background-color);
+ }
+ :host([secondary]) {
+ --background-color: var(--secondary-button-text-color);
+ --text-color: var(--secondary-button-background-color);
+ }
+ :host([link][secondary]) {
+ --text-color: var(--secondary-button-text-color);
}
- /* Keep below color definition for primary/secondary so that this takes
- precedence when disabled. */
+ /* Keep below color definition for primary so that this takes precedence
+ when disabled. */
:host([disabled]) {
- --background-color: #eaeaea;
- --button-color: #a8a8a8;
+ --background-color: var(--table-subheader-background-color);
+ --text-color: var(--deemphasized-text-color);
cursor: default;
}
@@ -86,9 +94,6 @@
:host([disabled][link]) {
--background-color: transparent;
}
- :host([link][tertiary]) {
- --button-color: var(--color-link-tertiary);
- }
/* Styles for the optional down arrow */
:host:not([down-arrow]) .downArrow {display: none; }
@@ -101,7 +106,7 @@
transition: border-top-color 200ms;
}
:host([down-arrow]) paper-button:hover .downArrow {
- border-top-color: #666;
+ border-top-color: var(--deemphasized-text-color);
}
</style>
<paper-button
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 6ac16cc7..ca6705e 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -31,21 +31,11 @@
value: false,
reflectToAttribute: true,
},
- raised: {
- type: Boolean,
- reflectToAttribute: true,
- computed: '_isRaised(link)',
- },
loading: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
- tertiary: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- },
disabled: {
type: Boolean,
observer: '_disabledChanged',
@@ -81,10 +71,6 @@
tabindex: '0',
},
- _isRaised(isLink) {
- return !isLink;
- },
-
_handleAction(e) {
if (this.disabled) {
e.preventDefault();
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
index 5e56db1..70b2635 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
@@ -28,7 +28,7 @@
cursor: pointer;
}
iron-icon.active {
- fill: var(--color-link);
+ fill: var(--link-color);
}
</style>
<button aria-label="Change star" on-tap="toggleStar">
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index 2832fa7..8efd309 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -20,7 +20,7 @@
const ChangeStates = {
MERGED: 'Merged',
ABANDONED: 'Abandoned',
- MERGE_CONFLIGT: 'Merge Conflict',
+ MERGE_CONFLICT: 'Merge Conflict',
WIP: 'WIP',
PRIVATE: 'Private',
};
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
index cb21bd2..1656c8e 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
@@ -32,7 +32,7 @@
max-height: 90vh;
}
header {
- border-bottom: 1px solid #cdcdcd;
+ border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
font-family: var(--font-family-bold);
}
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
index 99492d7..1090fea 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -27,6 +27,7 @@
<template>
<style include="shared-styles">
:host {
+ color: inherit;
display: inline;
}
</style>
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
index 689119f..7570533 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
@@ -44,7 +44,7 @@
font-family: var(--font-family-bold);
}
li[selected] gr-button {
- color: #000;
+ color: var(--primary-text-color);
font-family: var(--font-family-bold);
text-decoration: none;
}
@@ -55,8 +55,8 @@
.commands {
display: flex;
flex-direction: column;
- border-bottom: 1px solid #ddd;
- border-top: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color);
+ border-top: 1px solid var(--border-color);
padding: .5em;
}
gr-copy-clipboard {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
index ed2586a..0aa9ba6 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.html
@@ -43,7 +43,7 @@
padding: 0;
}
.dropdown-content {
- background-color: #fff;
+ background-color: var(--dropdown-background-color);
box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
max-height: 70vh;
margin-top: 2em;
@@ -63,18 +63,18 @@
min-height: 0;
padding: 10px 16px;
}
- --paper-item-selected: {
- background-color: rgba(161,194,250,.12);
- }
--paper-item-focused-before: {
- background-color: #f2f2f2;
+ background-color: var(--selection-background-color);
}
--paper-item-focused: {
- background-color: #f2f2f2;
+ background-color: var(--selection-background-color);
}
}
+ paper-item:hover {
+ background-color: var(--hover-background-color);
+ }
paper-item:not(:last-of-type) {
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color);
}
.bottomContent {
color: rgba(0,0,0,.54);
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
index 7f6cded..8a70b8b 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -22,6 +22,7 @@
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
<link rel="import" href="../../../styles/shared-styles.html">
<dom-module id="gr-dropdown">
@@ -35,7 +36,7 @@
width: 100%;
}
.dropdown-content {
- background-color: #fff;
+ background-color: var(--dropdown-background-color);
box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
}
gr-button {
@@ -52,10 +53,13 @@
ul {
list-style: none;
}
- ul .accountName {
- font-family: var(--font-family-bold);
+ .topContent,
+ li {
+ border-bottom: 1px solid var(--border-color);
}
- li .accountInfo,
+ li:last-of-type {
+ border: none;
+ }
li .itemAction {
cursor: pointer;
display: block;
@@ -73,17 +77,21 @@
text-decoration: none;
}
li .itemAction:not(.disabled):hover {
- background-color: #6B82D6;
- color: #fff;
+ background-color: var(--hover-background-color);
}
li:focus,
li.selected {
- background-color: #EBF5FB;
+ background-color: var(--selection-background-color);
outline: none;
}
+ li:focus .itemAction,
+ li.selected .itemAction {
+ background-color: transparent;
+ }
.topContent {
display: block;
padding: .85em 1em;
+ @apply --gr-dropdown-item;
}
.bold-text {
font-family: var(--font-family-bold);
@@ -125,19 +133,23 @@
as="link"
initial-count="75">
<li tabindex="-1">
- <span
- class$="itemAction [[_computeDisabledClass(link.id, disabledIds.*)]]"
- data-id$="[[link.id]]"
- on-tap="_handleItemTap"
- hidden$="[[link.url]]"
- tabindex="-1">[[link.name]]</span>
- <a
- class="itemAction"
- href$="[[_computeLinkURL(link)]]"
- rel$="[[_computeLinkRel(link)]]"
- target$="[[link.target]]"
- hidden$="[[!link.url]]"
- tabindex="-1">[[link.name]]</a>
+ <gr-tooltip-content
+ has-tooltip="[[_computeHasTooltip(link.tooltip)]]"
+ title$="[[link.tooltip]]">
+ <span
+ class$="itemAction [[_computeDisabledClass(link.id, disabledIds.*)]]"
+ data-id$="[[link.id]]"
+ on-tap="_handleItemTap"
+ hidden$="[[link.url]]"
+ tabindex="-1">[[link.name]]</span>
+ <a
+ class="itemAction"
+ href$="[[_computeLinkURL(link)]]"
+ rel$="[[_computeLinkRel(link)]]"
+ target$="[[link.target]]"
+ hidden$="[[!link.url]]"
+ tabindex="-1">[[link.name]]</a>
+ </gr-tooltip-content>
</li>
</template>
</ul>
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 f8edc83..70534f0 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -282,5 +282,9 @@
Polymer.dom.flush();
this._listElements = Polymer.dom(this.root).querySelectorAll('li');
},
+
+ _computeHasTooltip(tooltip) {
+ return !!tooltip;
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index d4d21b0..89b6068 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -134,6 +134,21 @@
assert.isFalse(tapped.called);
});
+ test('properly sets tooltips', () => {
+ element.items = [
+ {name: 'item one', id: 'foo', tooltip: 'hello'},
+ {name: 'item two', id: 'bar'},
+ ];
+ element.disabledIds = [];
+ flushAsynchronousOperations();
+ const tooltipContents = Polymer.dom(element.root)
+ .querySelectorAll('iron-dropdown li gr-tooltip-content');
+ assert.equal(tooltipContents.length, 2);
+ assert.isTrue(tooltipContents[0].hasTooltip);
+ assert.equal(tooltipContents[0].getAttribute('title'), 'hello');
+ assert.isFalse(tooltipContents[1].hasTooltip);
+ });
+
suite('keyboard navigation', () => {
setup(() => {
element.items = [
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
index 8addfb6..96066de 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
@@ -39,7 +39,7 @@
font: inherit;
}
label {
- color: #777;
+ color: var(--deemphasized-text-color);
display: inline-block;
font-family: var(--font-family-bold);
overflow: hidden;
@@ -48,14 +48,14 @@
@apply --label-style;
}
label.editable {
- color: var(--color-link);
+ color: var(--link-color);
cursor: pointer;
}
#dropdown {
box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
}
.inputContainer {
- background-color: #fff;
+ background-color: var(--dialog-background-color);
padding: .8em;
@apply --input-style;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
index ce43031..2c32709 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -93,10 +93,6 @@
].join(' ');
},
- _getScrollY() {
- return window.scrollY;
- },
-
unfloat() {
if (this.floatingDisabled) {
return;
@@ -127,26 +123,29 @@
this._reposition();
},
+ _getElementTop() {
+ return this.getBoundingClientRect().top;
+ },
+
_reposition() {
if (!this._headerFloating) {
return;
}
const header = this.$.header;
- const scrollY = this._topInitial - this._getScrollY();
+ // Since the outer element is relative positioned, can use its top
+ // to determine how to position the inner header element.
+ const elemTop = this._getElementTop();
let newTop;
- if (this.keepOnScroll) {
- if (scrollY > 0) {
- // Reposition to imitate natural scrolling.
- newTop = scrollY;
- } else {
- newTop = 0;
- }
- } else if (scrollY > -this._headerHeight ||
- this._topLast < -this._headerHeight) {
- // Allow to scroll away, but ignore when far behind the edge.
- newTop = scrollY;
+ if (this.keepOnScroll && elemTop < 0) {
+ // Should stick to the top.
+ newTop = 0;
} else {
- newTop = -this._headerHeight;
+ // Keep in line with the outer element.
+ newTop = elemTop;
+ }
+ // Initialize top style if it doesn't exist yet.
+ if (!header.style.top && this._topLast === newTop) {
+ header.style.top = newTop;
}
if (this._topLast !== newTop) {
if (newTop === undefined) {
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
index ec3ebe2..9eac7f7 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
@@ -74,24 +74,27 @@
};
const emulateScrollY = function(distance) {
- element._getScrollY.returns(distance);
+ element._getElementTop.returns(element._headerTopInitial - distance);
element._updateDebounced();
element.flushDebouncer('scroll');
};
setup(() => {
element._headerTopInitial = 10;
- sandbox.stub(element, '_getScrollY').returns(0);
+ sandbox.stub(element, '_getElementTop')
+ .returns(element._headerTopInitial);
});
test('scrolls header along with document', () => {
emulateScrollY(20);
- assert.equal(getHeaderTop(), '-12px');
+ // No top property is set when !_headerFloating.
+ assert.equal(getHeaderTop(), '');
});
test('does not stick to the top by default', () => {
emulateScrollY(150);
- assert.equal(getHeaderTop(), '-100px');
+ // No top property is set when !_headerFloating.
+ assert.equal(getHeaderTop(), '');
});
test('sticks to the top if enabled', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
index 7ab8f2a..e86ba4d 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
@@ -82,6 +82,7 @@
if (!el.content) { return; }
el.content.addEventListener('labels-changed', e => {
+ console.log('labels-changed', e.detail);
handler(e.detail);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index 3819e8a..d8a662e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -20,6 +20,7 @@
<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
<link rel="import" href="../../plugins/gr-admin-api/gr-admin-api.html">
<link rel="import" href="../../plugins/gr-attribute-helper/gr-attribute-helper.html">
+<link rel="import" href="../../plugins/gr-change-metadata-api/gr-change-metadata-api.html">
<link rel="import" href="../../plugins/gr-dom-hooks/gr-dom-hooks.html">
<link rel="import" href="../../plugins/gr-event-helper/gr-event-helper.html">
<link rel="import" href="../../plugins/gr-popup-interface/gr-popup-interface.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 78f9694..42bfc6a 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -60,9 +60,9 @@
});
element = fixture('basic');
errorStub = sandbox.stub(console, 'error');
- Gerrit._setPluginsCount(1);
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
+ Gerrit._setPluginsPending([]);
});
teardown(() => {
@@ -306,7 +306,6 @@
test('_setPluginsCount', done => {
stub('gr-reporting', {
pluginsLoaded() {
- assert.equal(Gerrit._pluginsPending, 0);
done();
},
});
@@ -324,13 +323,11 @@
test('_pluginInstalled', done => {
stub('gr-reporting', {
pluginsLoaded() {
- assert.equal(Gerrit._pluginsPending, 0);
done();
},
});
Gerrit._setPluginsCount(2);
Gerrit._pluginInstalled();
- assert.equal(Gerrit._pluginsPending, 1);
Gerrit._pluginInstalled();
});
@@ -348,10 +345,10 @@
assert.isTrue(Gerrit._pluginInstalled.calledOnce);
});
- test('install calls _pluginInstalled on error', () => {
- sandbox.stub(Gerrit, '_pluginInstalled');
+ test('plugin install errors mark plugins as loaded', () => {
+ Gerrit._setPluginsCount(1);
Gerrit.install(() => {}, '0.0pre-alpha');
- assert.isTrue(Gerrit._pluginInstalled.calledOnce);
+ return Gerrit.awaitPluginsLoaded();
});
test('installGwt calls _pluginInstalled', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
index 0e65065..57cbc85 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.js
@@ -17,13 +17,25 @@
(function(window) {
'use strict';
+ let restApi;
+
+ function getRestApi() {
+ if (!restApi) {
+ restApi = document.createElement('gr-rest-api-interface');
+ }
+ return restApi;
+ }
+
function GrPluginRestApi(opt_prefix) {
this.opt_prefix = opt_prefix || '';
- this._restApi = document.createElement('gr-rest-api-interface');
}
GrPluginRestApi.prototype.getLoggedIn = function() {
- return this._restApi.getLoggedIn();
+ return getRestApi().getLoggedIn();
+ };
+
+ GrPluginRestApi.prototype.getVersion = function() {
+ return getRestApi().getVersion();
};
/**
@@ -34,7 +46,7 @@
* @return {!Promise}
*/
GrPluginRestApi.prototype.fetch = function(method, url, opt_payload) {
- return this._restApi.send(method, this.opt_prefix + url, opt_payload);
+ return getRestApi().send(method, this.opt_prefix + url, opt_payload);
};
/**
@@ -55,7 +67,7 @@
}
});
} else {
- return this._restApi.getResponseObject(response);
+ return getRestApi().getResponseObject(response);
}
});
};
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
index 80460d6..5983621 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
@@ -30,20 +30,23 @@
let sandbox;
let getResponseObjectStub;
let sendStub;
+ let restApiStub;
setup(() => {
sandbox = sinon.sandbox.create();
getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
- stub('gr-rest-api-interface', {
- getAccount() {
- return Promise.resolve({name: 'Judy Hopps'});
- },
+ restApiStub = {
+ getAccount: () => Promise.resolve({name: 'Judy Hopps'}),
getResponseObject: getResponseObjectStub,
- send(...args) {
- return sendStub(...args);
- },
- });
+ send: sendStub,
+ getLoggedIn: sandbox.stub(),
+ getVersion: sandbox.stub(),
+ };
+ stub('gr-rest-api-interface', Object.keys(restApiStub).reduce((a, k) => {
+ a[k] = (...args) => restApiStub[k](...args);
+ return a;
+ }, {}));
Gerrit._setPluginsCount(1);
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
@@ -121,5 +124,21 @@
assert.equal('text', err);
});
});
+
+ test('getLoggedIn', () => {
+ restApiStub.getLoggedIn.returns(Promise.resolve(true));
+ return instance.getLoggedIn().then(result => {
+ assert.isTrue(restApiStub.getLoggedIn.calledOnce);
+ assert.isTrue(result);
+ });
+ });
+
+ test('getConfig', () => {
+ restApiStub.getVersion.returns(Promise.resolve('foo bar'));
+ return instance.getVersion().then(result => {
+ assert.isTrue(restApiStub.getVersion.calledOnce);
+ assert.equal(result, 'foo bar');
+ });
+ });
});
</script>
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 fbcf21af..60e07e0 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
@@ -20,13 +20,22 @@
/**
* Hash of loaded and installed plugins, name to Plugin object.
*/
- const plugins = {};
+ const _plugins = {};
+
+ /**
+ * Array of plugin URLs to be loaded, name to url.
+ */
+ let _pluginsPending = {};
+
+ let _pluginsPendingCount = -1;
const PANEL_ENDPOINTS_MAPPING = {
CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK: 'change-view-integration',
CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK: 'change-metadata-item',
};
+ const PLUGIN_LOADING_TIMEOUT_MS = 10000;
+
let _restAPI;
const getRestAPI = () => {
if (!_restAPI) {
@@ -80,6 +89,14 @@
window.$wnd = window;
function getPluginNameFromUrl(url) {
+ if (!(url instanceof URL)) {
+ try {
+ url = new URL(url);
+ } catch (e) {
+ console.warn(e);
+ return null;
+ }
+ }
const base = Gerrit.BaseUrlBehavior.getBaseUrl();
const pathname = url.pathname.replace(base, '');
// Site theme is server from predefined path.
@@ -223,6 +240,10 @@
return new GrRepoApi(this);
};
+ Plugin.prototype.changeMetadata = function() {
+ return new GrChangeMetadataApi(this);
+ };
+
Plugin.prototype.admin = function() {
return new GrAdminApi(this);
};
@@ -389,22 +410,26 @@
const Gerrit = window.Gerrit || {};
+ let _resolveAllPluginsLoaded = null;
+ let _allPluginsPromise = null;
+
+ Gerrit._endpoints = new GrPluginEndpoints();
+
// Provide reset plugins function to clear installed plugins between tests.
const app = document.querySelector('#app');
if (!app) {
// No gr-app found (running tests)
Gerrit._resetPlugins = () => {
- for (const k of Object.keys(plugins)) {
- delete plugins[k];
+ _resolveAllPluginsLoaded = null;
+ _allPluginsPromise = null;
+ Gerrit._setPluginsPending([]);
+ Gerrit._endpoints = new GrPluginEndpoints();
+ for (const k of Object.keys(_plugins)) {
+ delete _plugins[k];
}
};
}
- // Number of plugins to initialize, -1 means 'not yet known'.
- Gerrit._pluginsPending = -1;
-
- Gerrit._endpoints = new GrPluginEndpoints();
-
Gerrit.getPluginName = function() {
console.warn('Gerrit.getPluginName is not supported in PolyGerrit.',
'Please use plugin.getPluginName() instead.');
@@ -424,32 +449,31 @@
};
Gerrit.install = function(callback, opt_version, opt_src) {
+ // HTML import polyfill adds __importElement pointing to the import tag.
+ const script = document.currentScript &&
+ (document.currentScript.__importElement || document.currentScript);
+ const src = opt_src || (script && (script.src || script.baseURI));
+ const name = getPluginNameFromUrl(src);
+
if (opt_version && opt_version !== API_VERSION) {
- console.warn('Only version ' + API_VERSION +
- ' is supported in PolyGerrit. ' + opt_version + ' was given.');
- Gerrit._pluginInstalled();
+ Gerrit._pluginInstallError(`Plugin ${name} install error: only version ` +
+ API_VERSION + ' is supported in PolyGerrit. ' + opt_version +
+ ' was given.');
return;
}
- const src = opt_src || (document.currentScript &&
- (document.currentScript.src || document.currentScript.baseURI));
- const name = getPluginNameFromUrl(new URL(src));
- const existingPlugin = plugins[name];
+ const existingPlugin = _plugins[name];
const plugin = existingPlugin || new Plugin(src);
try {
callback(plugin);
- plugins[name] = plugin;
+ if (name) {
+ _plugins[name] = plugin;
+ }
+ if (!existingPlugin) {
+ Gerrit._pluginInstalled(src);
+ }
} catch (e) {
- console.warn(`${name} install failed: ${e.name}: ${e.message}`);
- }
- // Don't double count plugins that may have an html and js install.
- // TODO(beckysiegel) remove name check once name issue is resolved.
- // If there isn't a name, it's due to an issue with the polyfill for
- // html imports in Safari/Firefox. In this case, other plugin related
- // features may still be broken, but still make sure to call.
- // _pluginInstalled.
- if (!name || !existingPlugin) {
- Gerrit._pluginInstalled();
+ Gerrit._pluginInstallError(`${e.name}: ${e.message}`);
}
};
@@ -498,50 +522,85 @@
* @deprecated best effort support, will be removed with GWT UI.
*/
Gerrit.installGwt = function(url) {
- Gerrit._pluginInstalled();
- const name = getPluginNameFromUrl(new URL(url));
+ const name = getPluginNameFromUrl(url);
let plugin;
try {
- plugin = plugins[name] || new Plugin(url);
+ plugin = _plugins[name] || new Plugin(url);
plugin.deprecated.install();
+ Gerrit._pluginInstalled(url);
} catch (e) {
- console.warn(`${name} install failed: ${e.name}: ${e.message}`);
+ Gerrit._pluginInstallError(`${e.name}: ${e.message}`);
}
return plugin;
};
- Gerrit._allPluginsPromise = null;
- Gerrit._resolveAllPluginsLoaded = null;
-
Gerrit.awaitPluginsLoaded = function() {
- if (!Gerrit._allPluginsPromise) {
+ if (!_allPluginsPromise) {
if (Gerrit._arePluginsLoaded()) {
- Gerrit._allPluginsPromise = Promise.resolve();
+ _allPluginsPromise = Promise.resolve();
} else {
- Gerrit._allPluginsPromise = new Promise(resolve => {
- Gerrit._resolveAllPluginsLoaded = resolve;
- });
+ let timeoutId;
+ _allPluginsPromise =
+ Promise.race([
+ new Promise(resolve => _resolveAllPluginsLoaded = resolve),
+ new Promise(resolve => timeoutId = setTimeout(
+ Gerrit._pluginLoadingTimeout, PLUGIN_LOADING_TIMEOUT_MS)),
+ ]).then(() => clearTimeout(timeoutId));
}
}
- return Gerrit._allPluginsPromise;
+ return _allPluginsPromise;
+ };
+
+ Gerrit._pluginLoadingTimeout = function() {
+ document.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {
+ message: 'Plugins loading timeout. Check the console for errors.',
+ },
+ }));
+ console.error(`Failed to load plugins: ${Object.keys(_pluginsPending)}`);
+ Gerrit._setPluginsPending([]);
+ };
+
+ Gerrit._setPluginsPending = function(plugins) {
+ _pluginsPending = plugins.reduce((o, url) => {
+ o[getPluginNameFromUrl(url)] = url;
+ return o;
+ }, {});
+ Gerrit._setPluginsCount(plugins.length);
};
Gerrit._setPluginsCount = function(count) {
- Gerrit._pluginsPending = count;
+ _pluginsPendingCount = count;
if (Gerrit._arePluginsLoaded()) {
document.createElement('gr-reporting').pluginsLoaded();
- if (Gerrit._resolveAllPluginsLoaded) {
- Gerrit._resolveAllPluginsLoaded();
+ if (_resolveAllPluginsLoaded) {
+ _resolveAllPluginsLoaded();
}
}
};
- Gerrit._pluginInstalled = function() {
- Gerrit._setPluginsCount(Gerrit._pluginsPending - 1);
+ Gerrit._pluginInstallError = function(message) {
+ console.log(`Plugin install error: ${message}`);
+ Gerrit._setPluginsCount(_pluginsPendingCount - 1);
+ };
+
+ Gerrit._pluginInstalled = function(url) {
+ const name = getPluginNameFromUrl(url);
+ if (name && !_pluginsPending[name]) {
+ console.warn(`Unexpected plugin from ${url}!`);
+ } else {
+ if (name) {
+ delete _pluginsPending[name];
+ console.log(`Plugin ${name} installed`);
+ } else {
+ console.log(`Plugin installed from ${url}`);
+ }
+ Gerrit._setPluginsCount(_pluginsPendingCount - 1);
+ }
};
Gerrit._arePluginsLoaded = function() {
- return Gerrit._pluginsPending === 0;
+ return _pluginsPendingCount === 0;
};
Gerrit._getPluginScreenName = function(pluginName, screenName) {
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
index 41988de..5d7a8a8 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -31,7 +31,7 @@
}
.container {
align-items: center;
- background: #eee;
+ background: var(--header-background-color);
border-radius: .75em;
display: inline-flex;
padding: 0 .5em;
@@ -45,7 +45,7 @@
gr-button.remove {
--gr-button: {
border: 0;
- color: #666;
+ color: var(--deemphasized-text-color);
font-size: 1.7rem;
font-weight: normal;
height: .6em;
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
index d295770..71999acaf 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
@@ -37,7 +37,7 @@
display: none;
}
a {
- color: var(--default-text-color);
+ color: var(--primary-text-color);
text-decoration: none;
}
a:hover {
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
index e9bfb6d..ea94086 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
@@ -23,7 +23,7 @@
<template>
<style include="shared-styles">
:host {
- background: #fff;
+ background: var(--view-background-color);
box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
index 7806b8f..3885497 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
@@ -22,8 +22,8 @@
<template>
<style include="shared-styles">
#nav {
- background-color: #f5f5f5;
- border: 1px solid #eee;
+ background-color: var(--table-header-background-color);
+ border: 1px solid var(--border-color);
border-top: none;
height: 100%;
position: absolute;
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 bb9b627..a9fd05c 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
@@ -1051,13 +1051,12 @@
* @param {Defs.patchRange} patchRange
* @return {!Promise<!Array<!Object>>}
*/
- getChangeFilesAsSpeciallySortedArray(changeNum, patchRange) {
+ getChangeOrEditFiles(changeNum, patchRange) {
if (this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME)) {
return this.getChangeEditFiles(changeNum, patchRange).then(res =>
- this._normalizeChangeFilesResponse(res.files));
+ res.files);
}
- return this.getChangeFiles(changeNum, patchRange).then(
- this._normalizeChangeFilesResponse.bind(this));
+ return this.getChangeFiles(changeNum, patchRange);
},
/**
@@ -1071,25 +1070,6 @@
});
},
- /**
- * The closure compiler doesn't realize this.specialFilePathCompare is
- * valid.
- * @suppress {checkTypes}
- */
- _normalizeChangeFilesResponse(response) {
- if (!response) { return []; }
- const paths = Object.keys(response).sort(this.specialFilePathCompare);
- const files = [];
- for (let i = 0; i < paths.length; i++) {
- const info = response[paths[i]];
- info.__path = paths[i];
- info.lines_inserted = info.lines_inserted || 0;
- info.lines_deleted = info.lines_deleted || 0;
- files.push(info);
- }
- return files;
- },
-
getChangeRevisionActions(changeNum, patchNum) {
return this._getChangeURLAndFetch(changeNum, '/actions', patchNum)
.then(revisionActions => {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 95a35e4..fb20da4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -1295,8 +1295,8 @@
});
});
- test('getChangeFilesAsSpeciallySortedArray is edit-sensitive', () => {
- const fn = element.getChangeFilesAsSpeciallySortedArray.bind(element);
+ test('getChangeFilesOrEditFiles is edit-sensitive', () => {
+ const fn = element.getChangeOrEditFiles.bind(element);
const getChangeFilesStub = sandbox.stub(element, 'getChangeFiles')
.returns(Promise.resolve({}));
const getChangeEditFilesStub = sandbox.stub(element, 'getChangeEditFiles')
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
index e85fe38..cf39b5a 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
@@ -41,7 +41,6 @@
display: inline-block
}
#textarea {
- background-color: var(--background-color, none);
width: 100%;
}
#hiddenText #emojiSuggestions {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index c70dc8d..a3da7d8 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -70,10 +70,6 @@
notify: true,
observer: '_handleTextChanged',
},
- backgroundColor: {
- type: String,
- value: '#fff',
- },
hideBorder: {
type: Boolean,
value: false,
@@ -123,9 +119,6 @@
if (this.hideBorder) {
this.$.textarea.classList.add('noBorder');
}
- if (this.backgroundColor) {
- this.updateStyles({'--background-color': this.backgroundColor});
- }
},
closeDropdown() {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index 97f8ca4..3a52543 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -25,7 +25,6 @@
<link rel="import" href="gr-textarea.html">
<script>void(0);</script>
-
<test-fixture id="basic">
<template>
<gr-textarea></gr-textarea>
@@ -60,15 +59,6 @@
assert.isTrue(element.$.textarea.classList.contains('noBorder'));
});
- test('background color is set properly', () => {
- assert.equal(getComputedStyle(element.$.textarea).backgroundColor,
- 'rgb(255, 255, 255)');
- element.backgroundColor = 'pink';
- element.ready();
- assert.equal(getComputedStyle(element.$.textarea).backgroundColor,
- 'rgb(255, 192, 203)');
- });
-
test('emoji selector is not open with the textarea lacks focus', () => {
element.$.textarea.selectionStart = 1;
element.$.textarea.selectionEnd = 1;
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html
deleted file mode 100644
index 242c04b..0000000
--- a/polygerrit-ui/app/index.html
+++ /dev/null
@@ -1,49 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<html lang="en">
-<meta charset="utf-8">
-<meta name="description" content="Gerrit Code Review">
-<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">
-
-<!--
-RobotoMono fonts are used in styles/fonts.css
-@see https://github.com/w3c/preload/issues/32 regarding crossorigin
--->
-<link rel="preload" href="/fonts/RobotoMono-Regular.woff2" as="font" type="font/woff2" crossorigin>
-<link rel="preload" href="/fonts/RobotoMono-Regular.woff" as="font" type="font/woff" crossorigin>
-<link rel="preload" href="/fonts/Roboto-Regular.woff2" as="font" type="font/woff2" crossorigin>
-<link rel="preload" href="/fonts/Roboto-Regular.woff" as="font" type="font/woff" crossorigin>
-<link rel="preload" href="/fonts/Roboto-Medium.woff2" as="font" type="font/woff2" crossorigin>
-<link rel="preload" href="/fonts/Roboto-Medium.woff" as="font" type="font/woff" crossorigin>
-<link rel="stylesheet" href="/styles/fonts.css">
-<link rel="stylesheet" href="/styles/main.css">
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<!--
- - Content between webcomponents-lite and the load of the main app element
- - run before polymer-resin is installed so may have security consequences.
- - Contact your local security engineer if you have any questions, and
- - CC them on any changes that load content before gr-app.html.
- -
- - github.com/Polymer/polymer-resin/blob/master/getting-started.md#integrating
- -->
-<link rel="preload" href="/elements/gr-app.js" as="script" crossorigin="anonymous">
-<link rel="import" href="/elements/gr-app.html">
-
-<body unresolved>
-<gr-app id="app"></gr-app>
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 0d03910..b60aa22 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -65,7 +65,6 @@
name = name + "_top_sources",
srcs = [
"favicon.ico",
- "index.html",
],
)
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
index 61142cb..9642019 100644
--- a/polygerrit-ui/app/styles/app-theme.html
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -22,31 +22,72 @@
--header-title-content: 'Gerrit';
--header-icon: none;
--header-icon-size: 0em;
+ --header-text-color: #000;
--footer-background-color: var(--header-background-color);
+ --border-color: #ddd;
/* Following are not part of plugin API. */
- --search-border-color: #ddd;
- --selection-background-color: #ebf5fb;
- --default-text-color: #000;
+ --selection-background-color: #f1f5fb;
+ --hover-background-color: #e8effa;
+ --expanded-background-color: #eee;
--view-background-color: #fff;
--default-horizontal-margin: 1rem;
+ --deemphasized-text-color: #757575;
--font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--font-family-bold: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--monospace-font-family: 'Roboto Mono', Menlo, 'Lucida Console', Monaco, monospace;
--iron-overlay-backdrop: {
transition: none;
}
+ --table-header-background-color: #fafafa;
+ --table-subheader-background-color: #eaeaea;
+
+ --dropdown-background-color: #fff;
/* Font sizes */
--font-size-normal: 1rem;
--font-size-small: .92rem;
--font-size-large: 1.076rem;
- /* Follow are a part of the design refresh */
- --color-link: #2a66d9;
- --color-link-tertiary: #000;
- /* 12% darker */
- --color-button-hover: #0B47BA;
+ --link-color: #2a66d9;
+ --primary-button-background-color: var(--link-color);
+ --primary-button-text-color: #fff;
+ --secondary-button-background-color: #fff;
+ --secondary-button-text-color: #212121;
+ --default-button-background-color: #fff;
+ --default-button-text-color: var(--link-color);
+
+ /* Used for both the old patchset header and for indicating that a particular
+ change message was selected. */
+ --emphasis-color: #fff9c4;
+
+ --error-text-color: red;
+
+ --diff-highlight-range-color: rgba(255, 213, 0, 0.5);
+ --diff-highlight-range-hover-color: rgba(255, 255, 0, 0.5);
+
+ --syntax-default-color: var(--primary-text-color);
+ --syntax-meta-color: #FF1717;
+ --syntax-keyword-color: #9E0069;
+ --syntax-number-color: #164;
+ --syntax-selector-class-color: #164;
+ --syntax-variable-color: black;
+ --syntax-template-variable-color: #0000C0;
+ --syntax-comment-color: #3F7F5F;
+ --syntax-string-color: #2A00FF;
+ --syntax-selector-id-color: #2A00FF;
+ --syntax-built_in-color: #30a;
+ --syntax-tag-color: #170;
+ --syntax-link-color: #219;
+ --syntax-meta-keyword-color: #219;
+ --syntax-type-color: var(--color-link);
+ --syntax-title-color: #0000C0;
+ --syntax-attr-color: #219;
+ --syntax-literal-color: #219;
+ --syntax-selector-pseudo-color: #FA8602;
+ --syntax-regexp-color: #FA8602;
+ --syntax-selector-attr-color: #FA8602;
+ --syntax-template-tag-color: #FA8602;
}
@media screen and (max-width: 50em) {
:root {
diff --git a/polygerrit-ui/app/styles/dashboard-header-styles.html b/polygerrit-ui/app/styles/dashboard-header-styles.html
new file mode 100644
index 0000000..a88f68c
--- /dev/null
+++ b/polygerrit-ui/app/styles/dashboard-header-styles.html
@@ -0,0 +1,48 @@
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<dom-module id="dashboard-header-styles">
+ <template>
+ <style>
+ :host {
+ background-color: var(--view-background-color);
+ display: block;
+ height: 9em;
+ width: 100%;
+ }
+ gr-avatar {
+ display: inline-block;
+ height: 7em;
+ left: 1em;
+ margin: 1em;
+ top: 1em;
+ width: 7em;
+ }
+ .info {
+ display: inline-block;
+ padding: 1em;
+ vertical-align: top;
+ }
+ .info > div > span {
+ display: inline-block;
+ font-weight: bold;
+ text-align: right;
+ width: 4em;
+ }
+ </style>
+ </template>
+</dom-module>
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index 76a8c1b..4f92039 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -22,7 +22,28 @@
}
gr-change-list-item,
tr {
- border-bottom: 1px solid #ddd;
+ border-top: 1px solid var(--border-color);
+ }
+ gr-change-list-item[selected],
+ gr-change-list-item:focus {
+ background-color: var(--selection-background-color);
+ }
+ /* The border-collapse attribute only works on sibling elements, not
+ cousin elements. So, if we want the table to have a sticky header and
+ have borders between each row, we must disable the border-top on the
+ elements directly below a .topHeader. */
+ .topHeader ~ gr-change-list-item:first-of-type,
+ .topHeader + .groupHeader {
+ border-top: none;
+ }
+ /* Needed to show a border on top of the first gr-change-list-item when a
+ groupHeader exists. Cannot use + selector because of dom-repeats
+ existing in the DOM tree. */
+ .topHeader ~ .groupHeader ~ gr-change-list-item {
+ border-top: 1px solid var(--border-color);
+ }
+ tbody {
+ border-bottom: 1px solid var(--border-color);
}
tr.topHeader {
border: none;
@@ -46,7 +67,7 @@
font-family: var(--font-family-bold);
}
.topHeader th {
- background-color: #fafafa;
+ background-color: var(--table-header-background-color);
font-size: var(--font-size-large);
height: 3rem;
position: -webkit-sticky;
@@ -57,7 +78,7 @@
/* :after pseudoelements are used here because borders on sticky table
headers with a background color are broken. */
th:after {
- border-bottom: 1px solid #ddd;
+ border-bottom: 1px solid var(--border-color);
bottom: 0;
content: '';
left: 0;
@@ -65,14 +86,14 @@
width: 100%;
}
th.label:after {
- border-left: 1px solid #ddd;
+ border-left: 1px solid var(--border-color);
top: 0;
}
.groupHeader {
- background-color: #eaeaea;
+ background-color: var(--table-subheader-background-color);
}
.groupHeader a {
- color: #000;
+ color: var(--primary-text-color);
text-decoration: none;
}
.groupHeader a:hover {
@@ -81,14 +102,12 @@
.cell {
height: 2.25rem;
}
- .keyboard,
.star {
padding: 0;
}
gr-change-star {
vertical-align: middle;
}
- .keyboard,
.branch,
.star,
.label,
@@ -101,18 +120,17 @@
.project {
white-space: nowrap;
}
- .keyboard,
.star {
vertical-align: middle;
}
- .keyboard {
+ .leftPadding {
width: 20px;
}
.star {
width: 30px;
}
.label {
- border-left: 1px solid #ddd;
+ border-left: 1px solid var(--border-color);
text-align: center;
width: 3rem;
}
@@ -122,7 +140,7 @@
.truncatedProject {
display: none;
}
- @media only screen and (max-width: 90em) {
+ @media only screen and (max-width: 100em) {
.assignee,
.branch,
.owner {
@@ -146,12 +164,21 @@
justify-content: space-between;
padding: .25em .5em;
}
+ gr-change-list-item[selected],
+ gr-change-list-item:focus {
+ background-color: var(--view-background-color);
+ border: none;
+ border-top: 1px solid var(--border-color);
+ }
+ gr-change-list-item:hover {
+ background-color: var(--view-background-color);
+ }
.cell {
align-items: center;
display: flex;
}
.topHeader,
- .keyboard,
+ .leftPadding,
.status,
.project,
.branch,
@@ -176,8 +203,8 @@
}
}
@media only screen and (min-width: 1450px) {
- .project {
- width: 20em;
+ :host {
+ font-size: 14px;
}
}
</style>
diff --git a/polygerrit-ui/app/styles/gr-form-styles.html b/polygerrit-ui/app/styles/gr-form-styles.html
index 0417e91..88c75c8 100644
--- a/polygerrit-ui/app/styles/gr-form-styles.html
+++ b/polygerrit-ui/app/styles/gr-form-styles.html
@@ -37,7 +37,7 @@
display: inline-block;
}
.gr-form-styles .title {
- color: #666;
+ color: var(--deemphasized-text-color);
font-family: var(--font-family-bold);
padding-right: .5em;
width: 15em;
@@ -46,7 +46,7 @@
font-size: var(--font-size-normal);
}
.gr-form-styles th {
- color: #666;
+ color: var(--deemphasized-text-color);
text-align: left;
vertical-align: bottom;
}
@@ -76,7 +76,7 @@
.gr-form-styles input:not([type="checkbox"]),
.gr-form-styles select,
.gr-form-styles textarea {
- border: 1px solid #d1d2d3;
+ border: 1px solid var(--border-color);
border-radius: 2px;
font-size: var(--font-size-normal);
height: 2em;
@@ -94,7 +94,7 @@
height: auto;
min-height: 2em;
--iron-autogrow-textarea: {
- border: 1px solid #d1d2d3;
+ border: 1px solid var(--border-color);
border-radius: 2px;
box-sizing: border-box;
font-size: var(--font-size-normal);
@@ -104,7 +104,7 @@
.gr-form-styles gr-autocomplete {
border: none;
--gr-autocomplete: {
- border: 1px solid #d1d2d3;
+ border: 1px solid var(--border-color);
border-radius: 2px;
font-size: var(--font-size-normal);
height: 2em;
diff --git a/polygerrit-ui/app/styles/gr-menu-page-styles.html b/polygerrit-ui/app/styles/gr-menu-page-styles.html
index bafcbc6..4adbeda 100644
--- a/polygerrit-ui/app/styles/gr-menu-page-styles.html
+++ b/polygerrit-ui/app/styles/gr-menu-page-styles.html
@@ -25,7 +25,12 @@
margin: 2em auto;
max-width: 50em;
}
- main.table {
+ .mainHeader {
+ margin-left: 14em;
+ padding: 1em 0 1em 2em;
+ }
+ main.table,
+ .mainHeader {
margin-top: 0;
margin-right: 0;
margin-left: 14em;
@@ -36,7 +41,7 @@
content: ' *';
}
.loading {
- color: #666;
+ color: var(--deemphasized-text-color);
padding: 1em var(--default-horizontal-margin);
}
@media only screen and (max-width: 67em) {
@@ -57,6 +62,10 @@
main.table {
margin: 0;
}
+ .mainHeader {
+ margin-left: 0;
+ padding: .5em 0 .5em 1em;
+ }
}
</style>
</template>
diff --git a/polygerrit-ui/app/styles/gr-page-nav-styles.html b/polygerrit-ui/app/styles/gr-page-nav-styles.html
index 8d8659e..6d62f23 100644
--- a/polygerrit-ui/app/styles/gr-page-nav-styles.html
+++ b/polygerrit-ui/app/styles/gr-page-nav-styles.html
@@ -49,13 +49,13 @@
margin: .4em 0;
}
.navStyles .selected {
- background-color: #fff;
+ background-color: var(--view-background-color);
border-bottom: 1px dotted #808080;
border-top: 1px dotted #808080;
font-family: var(--font-family-bold);
}
.navStyles a {
- color: black;
+ color: var(--primary-text-color);
display: inline-block;
margin: .4em 0;
}
diff --git a/polygerrit-ui/app/styles/gr-subpage-styles.html b/polygerrit-ui/app/styles/gr-subpage-styles.html
new file mode 100644
index 0000000..098a604
--- /dev/null
+++ b/polygerrit-ui/app/styles/gr-subpage-styles.html
@@ -0,0 +1,34 @@
+<!--
+@license
+Copyright (C) 2018 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<dom-module id="gr-subpage-styles">
+ <template>
+ <style>
+ main {
+ margin: 1em 1em;
+ }
+ .loading {
+ display: none;
+ }
+ #loading.loading {
+ display: block;
+ }
+ #loading:not(.loading) {
+ display: none;
+ }
+ </style>
+ </template>
+</dom-module>
diff --git a/polygerrit-ui/app/styles/gr-table-styles.html b/polygerrit-ui/app/styles/gr-table-styles.html
index 7b4d856..2bcd743 100644
--- a/polygerrit-ui/app/styles/gr-table-styles.html
+++ b/polygerrit-ui/app/styles/gr-table-styles.html
@@ -26,15 +26,15 @@
width: 100%;
}
.genericList tr.table {
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid var(--border-color);
}
.genericList td {
flex-shrink: 0;
padding: .3em .5em;
}
.genericList th {
- background-color: #ddd;
- border-bottom: 1px solid #eee;
+ background-color: var(--border-color);
+ border-bottom: 1px solid var(--border-color);
font-family: var(--font-family-bold);
padding: .3em .5em;
text-align: left;
@@ -43,7 +43,7 @@
background-color: #eee;
}
.genericList a {
- color: var(--default-text-color);
+ color: var(--primary-text-color);
text-decoration: none;
}
.genericList a:hover {
@@ -53,7 +53,7 @@
width: 70%;
}
.genericList .loadingMsg {
- color: #666;
+ color: var(--deemphasized-text-color);
display: block;
padding: 1em var(--default-horizontal-margin);
}
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html
index 8917fd7..f97408d 100644
--- a/polygerrit-ui/app/styles/shared-styles.html
+++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -35,12 +35,13 @@
}
input,
iron-autogrow-textarea {
+ background-color: inherit;
box-sizing: border-box;
margin: 0;
padding: 0;
}
a {
- color: var(--color-link);
+ color: var(--link-color);
}
input,
textarea,
@@ -80,7 +81,7 @@
font-family: var(--font-family-bold);
}
iron-icon {
- color: #757575;
+ color: var(--deemphasized-text-color);
--iron-icon-height: 20px;
--iron-icon-width: 20px;
}
@@ -89,21 +90,23 @@
display: none !important;
}
.separator {
- border-left: 1px solid rgba(0, 0, 0, .3);
+ border-left: 1px solid var(--deemphasized-text-color);
height: 20px;
margin: 0 8px;
-
}
.separator.transparent {
border-color: transparent;
}
paper-toggle-button {
- --paper-toggle-button-checked-bar-color: var(--color-link);
- --paper-toggle-button-checked-button-color: var(--color-link);
+ --paper-toggle-button-checked-bar-color: var(--link-color);
+ --paper-toggle-button-checked-button-color: var(--link-color);
}
strong {
font-family: var(--font-family-bold);
}
+ :host {
+ color: var(--primary-text-color);
+ }
</style>
</template>
</dom-module>
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html
index 246e7b8..6b3a4d1 100644
--- a/polygerrit-ui/app/test/common-test-setup.html
+++ b/polygerrit-ui/app/test/common-test-setup.html
@@ -49,14 +49,9 @@
(function() {
setup(() => {
if (!window.Gerrit) { return; }
- Gerrit._pluginsPending = -1;
- Gerrit._allPluginsPromise = undefined;
if (Gerrit._resetPlugins) {
Gerrit._resetPlugins();
}
- if (Gerrit._endpoints) {
- Gerrit._endpoints = new GrPluginEndpoints();
- }
});
})();
</script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index c29e3b5..6cf674a 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -163,6 +163,7 @@
'shared/gr-js-api-interface/gr-js-api-interface_test.html',
'shared/gr-js-api-interface/gr-plugin-endpoints_test.html',
'shared/gr-js-api-interface/gr-plugin-rest-api_test.html',
+ 'shared/gr-fixed-panel/gr-fixed-panel_test.html',
'shared/gr-limited-text/gr-limited-text_test.html',
'shared/gr-linked-chip/gr-linked-chip_test.html',
'shared/gr-linked-text/gr-linked-text_test.html',
@@ -195,6 +196,7 @@
'keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html',
'rest-client-behavior/rest-client-behavior_test.html',
'gr-access-behavior/gr-access-behavior_test.html',
+ 'gr-admin-nav-behavior/gr-admin-nav-behavior_test.html',
'gr-anonymous-name-behavior/gr-anonymous-name-behavior_test.html',
'gr-change-table-behavior/gr-change-table-behavior_test.html',
'gr-patch-set-behavior/gr-patch-set-behavior_test.html',
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index bc4a4d1..2594ff7 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -20,6 +20,7 @@
"encoding/json"
"errors"
"flag"
+ "github.com/robfig/soy"
"io"
"io/ioutil"
"log"
@@ -36,11 +37,17 @@
prod = flag.Bool("prod", false, "Serve production assets")
scheme = flag.String("scheme", "https", "URL scheme")
plugins = flag.String("plugins", "", "comma seperated plugin paths to serve")
+
+ tofu, _ = soy.NewBundle().
+ AddTemplateFile("../resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy").
+ CompileToTofu()
)
func main() {
flag.Parse()
+ http.HandleFunc("/index.html", handleIndex)
+
if *prod {
http.Handle("/", http.FileServer(http.Dir("dist")))
} else {
@@ -63,6 +70,15 @@
log.Fatal(http.ListenAndServe(*port, &server{}))
}
+func handleIndex(w http.ResponseWriter, r *http.Request) {
+ var obj = map[string]interface{}{
+ "canonicalPath": "",
+ "staticResourcePath": "",
+ }
+ w.Header().Set("Content-Type", "text/html")
+ tofu.Render(w, "com.google.gerrit.httpd.raw.Index", obj)
+}
+
func handleRESTProxy(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, ".html") {
w.Header().Set("Content-Type", "text/html")
@@ -211,13 +227,13 @@
func (_ *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s %s %s\n", r.Proto, r.Method, r.RemoteAddr, r.URL)
for _, prefix := range fePaths {
- if strings.HasPrefix(r.URL.Path, prefix) {
- r.URL.Path = "/"
- log.Println("Redirecting to /")
+ if strings.HasPrefix(r.URL.Path, prefix) || r.URL.Path == "/" {
+ r.URL.Path = "/index.html"
+ log.Println("Redirecting to /index.html")
break
} else if match := issueNumRE.Find([]byte(r.URL.Path)); match != nil {
- r.URL.Path = "/"
- log.Println("Redirecting to /")
+ r.URL.Path = "/index.html"
+ log.Println("Redirecting to /index.html")
break
}
}
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index a013140..699dd0e 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -29,12 +29,11 @@
<meta name="description" content="Gerrit Code Review">{\n}
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">{\n}
- {if $canonicalPath != '' or $versionInfo}
- <script>
- {if $canonicalPath != ''}window.CANONICAL_PATH = '{$canonicalPath}';{/if}
- {if $versionInfo}window.VERSION_INFO = '{$versionInfo}';{/if}
- </script>{\n}
- {/if}
+ <script>
+ window.CLOSURE_NO_DEPS = true;
+ {if $canonicalPath != ''}window.CANONICAL_PATH = '{$canonicalPath}';{/if}
+ {if $versionInfo}window.VERSION_INFO = '{$versionInfo}';{/if}
+ </script>{\n}
{if $faviconPath}
<link rel="icon" type="image/x-icon" href="{$canonicalPath}/{$faviconPath}">{\n}
diff --git a/resources/com/google/gerrit/server/config/CapabilityConstants.properties b/resources/com/google/gerrit/server/config/CapabilityConstants.properties
index 1de7eea..6654837 100644
--- a/resources/com/google/gerrit/server/config/CapabilityConstants.properties
+++ b/resources/com/google/gerrit/server/config/CapabilityConstants.properties
@@ -19,3 +19,4 @@
viewConnections = View Connections
viewPlugins = View Plugins
viewQueue = View Queue
+viewAccess = View Access
diff --git a/resources/com/google/gerrit/server/mail/Reverted.soy b/resources/com/google/gerrit/server/mail/Reverted.soy
index 09e32ff..fba8744 100644
--- a/resources/com/google/gerrit/server/mail/Reverted.soy
+++ b/resources/com/google/gerrit/server/mail/Reverted.soy
@@ -25,7 +25,7 @@
* @param fromName
*/
{template .Reverted kind="text"}
- {$fromName} has reverted this change.
+ {$fromName} has created a revert of this change.
{if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
{\n}
Change subject: {$change.subject}{\n}
diff --git a/resources/com/google/gerrit/server/mail/RevertedHtml.soy b/resources/com/google/gerrit/server/mail/RevertedHtml.soy
index 63ad6f0..b7b254e 100644
--- a/resources/com/google/gerrit/server/mail/RevertedHtml.soy
+++ b/resources/com/google/gerrit/server/mail/RevertedHtml.soy
@@ -22,7 +22,7 @@
*/
{template .RevertedHtml}
<p>
- {$fromName} <strong>reverted</strong> this change.
+ {$fromName} has <strong>created a revert</strong> of this change.
</p>
{if $email.changeUrl}
diff --git a/tools/bzl/javadoc.bzl b/tools/bzl/javadoc.bzl
index 97f291b..f49c881 100644
--- a/tools/bzl/javadoc.bzl
+++ b/tools/bzl/javadoc.bzl
@@ -28,6 +28,8 @@
source = ctx.outputs.zip.path + ".source"
external_docs = ["http://docs.oracle.com/javase/8/docs/api"] + ctx.attr.external_docs
cmd = [
+ "TZ=UTC",
+ "export TZ",
"rm -rf %s" % source,
"mkdir %s" % source,
" && ".join(["unzip -qud %s %s" % (source, j.path) for j in source_jars]),
@@ -50,7 +52,7 @@
":".join(transitive_jar_paths),
"-d %s" % dir]),
"find %s -exec touch -t 198001010000 '{}' ';'" % dir,
- "(cd %s && zip -qr ../%s *)" % (dir, ctx.outputs.zip.basename),
+ "(cd %s && zip -Xqr ../%s *)" % (dir, ctx.outputs.zip.basename),
]
ctx.actions.run_shell(
inputs = list(transitive_jar_set) + list(source_jars) + ctx.files._jdk,
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 945b8e1..2796f64 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -75,13 +75,15 @@
_bash(ctx, " && " .join([
"TMP=$(mktemp -d || mktemp -d -t bazel-tmp)",
+ "TZ=UTC",
+ "export UTC",
"cd $TMP",
"mkdir bower_components",
"cd bower_components",
"unzip %s" % ctx.path(download_name),
"cd ..",
"find . -exec touch -t 198001010000 '{}' ';'",
- "zip -r %s bower_components" % renamed_name,
+ "zip -Xr %s bower_components" % renamed_name,
"cd ..",
"rm -rf ${TMP}",
]))
@@ -153,11 +155,13 @@
name = name[:-4]
dest = "%s/%s" % (dir, name)
cmd = " && ".join([
+ "TZ=UTC",
+ "export TZ",
"mkdir -p %s" % dest,
"cp %s %s/" % (' '.join([s.path for s in ctx.files.srcs]), dest),
"cd %s" % dir,
"find . -exec touch -t 198001010000 '{}' ';'",
- "zip -qr ../%s *" % ctx.outputs.zip.basename
+ "zip -Xqr ../%s *" % ctx.outputs.zip.basename
])
ctx.actions.run_shell(
@@ -232,13 +236,15 @@
outputs=[out_zip],
command=" && ".join([
"p=$PWD",
+ "TZ=UTC",
+ "export TZ",
"rm -rf %s.dir" % out_zip.path,
"mkdir -p %s.dir/bower_components" % out_zip.path,
"cd %s.dir/bower_components" % out_zip.path,
"for z in %s; do unzip -q $p/$z ; done" % " ".join(sorted([z.path for z in zips])),
"cd ..",
"find . -exec touch -t 198001010000 '{}' ';'",
- "zip -qr $p/%s bower_components/*" % out_zip.path,
+ "zip -Xqr $p/%s bower_components/*" % out_zip.path,
]),
mnemonic="BowerCombine")
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index a8ccbee..d6a4c78 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -55,9 +55,11 @@
def _make_war(input_dir, output):
return '(%s)' % ' && '.join([
'root=$(pwd)',
+ 'TZ=UTC',
+ 'export TZ',
'cd %s' % input_dir,
"find . -exec touch -t 198001010000 '{}' ';' 2> /dev/null",
- 'zip -9qr ${root}/%s .' % (output.path),
+ 'zip -X -9qr ${root}/%s .' % (output.path),
])
def _war_impl(ctx):
diff --git a/tools/setup_gjf.sh b/tools/setup_gjf.sh
index b3ac143..9c36c72 100755
--- a/tools/setup_gjf.sh
+++ b/tools/setup_gjf.sh
@@ -17,7 +17,7 @@
set -eu
# Keep this version in sync with dev-contributing.txt.
-VERSION=${1:-1.3}
+VERSION=${1:-1.5}
case "$VERSION" in
1.3)