Merge "Adding checkbox to annotations API to toggle annotations on/off"
diff --git a/.gitignore b/.gitignore
index 0e954ce..5fdc85b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,7 +20,6 @@
/.settings/org.maven.ide.eclipse.prefs
/bazel-*
/bin/
-/buck-out
/eclipse-out
/extras
/gerrit-package-plugins
@@ -30,4 +29,4 @@
/plugins/cookbook-plugin/
/test_site
/tools/format
-/.vscode
\ No newline at end of file
+/.vscode
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index a4f03d5..cc03334 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1422,6 +1422,177 @@
link:cmd-show-queue.html[look at the Gerrit task queue via ssh].
+[[reference]]
+== Permission evaluation reference
+
+Permission evaluation is expressed in the following concepts:
+
+* PermisssionRule: a single combination of {ALLOW, DENY, BLOCK} and
+group, and optionally a vote range and an 'exclusive' bit.
+
+* Permission: groups PermissionRule by permission name. All
+PermissionRules for same access type (eg. "read", "push") are grouped
+into a Permission implicitly. The exclusive bit lives here.
+
+* AccessSection: ties a list of Permissions to a single ref pattern.
+Each AccessSection comes from a single project.
+
+
+
+Here is how these play out in a link:config-project-config.html[project.config] file:
+
+----
+ # An AccessSection
+ [access "refs/heads/stable/*"]
+ exclusiveGroupPermissions = create
+
+ # Each of the following lines corresponds to a PermissionRule
+ # The next two PermissionRule together form the "read" Permission
+ read = group Administrators
+ read = group Registered Users
+
+ # A Permission with a block and block-override
+ create = block group Registered Users
+ create = group Project Owners
+
+ # A Permission and PermissionRule for a label
+ label-Code-Review = -2..+2 group Project Owners
+----
+
+=== Ref permissions
+
+Access to refs can be blocked, allowed or denied.
+
+==== BLOCK
+
+For blocking access, all rules marked BLOCK are tested, and if one
+such rule matches, the user is denied access.
+
+The rules are ordered by inheritance, starting from All-Projects down.
+Within a project, more specific ref patterns come first. The downward
+ordering lets administrators enforce access rules across all projects
+in a site.
+
+BLOCK rules can have exceptions defined on the same project (eg. BLOCK
+anonymous users, ie. everyone, but make an exception for Admin users),
+either by:
+
+1. adding ALLOW PermissionRules in the same Permission. This implies
+they apply to the same ref pattern.
+
+2. adding an ALLOW Permission in the same project with a more specific
+ref pattern, but marked "exclusive". This allows them to apply to
+different ref patterns.
+
+Such additions not only bypass BLOCK rules, but they will also grant
+permissions when they are processed in the ALLOW/DENY processing, as
+described in the next subsection.
+
+==== ALLOW
+
+For allowing access, all ALLOW/DENY rules that might apply to a ref
+are tested until one granting access is found, or until either an
+"exclusive" rule ends the search, or all rules have been tested.
+
+The rules are ordered from specific ref patterns to general patterns,
+and for equally specific patterns, from originating project up to
+All-Projects.
+
+This ordering lets project owners apply permissions specific to their
+project, overwriting the site defaults specified in All-Projects.
+
+==== DENY
+
+DENY is processed together with ALLOW.
+
+As said, during ALLOW/DENY processing, rules are tried out one by one.
+For each (permission, ref-pattern, group) only a single rule
+ALLOW/DENY rule is picked. If that first rule is a DENY rule, any
+following ALLOW rules for the same (permission, ref-pattern, group)
+will be ignored, canceling out their effect.
+
+DENY is confusing because it only works on a specific (ref-pattern,
+group) pair. The parent project can undo the effect of a DENY rule by
+introducing an extra rule which features a more general ref pattern or
+a different group.
+
+==== DENY/ALLOW example
+
+Consider the ref "refs/a" and the following configuration:
+----
+
+child-project: project.config
+ [access "refs/a"]
+ read = deny group A
+
+All-Projects: project.config
+ [access "refs/a"]
+ read = group A # ALLOW
+ [access "refs/*"]
+ read = group B # ALLOW
+----
+
+When determining access, first "read = DENY group A" on "refs/a" is
+encountered. The following rule to consider is "ALLOW read group A" on
+"refs/a". The latter rule applies to the same (permission,
+ref-pattern, group) tuple, so it it is ignored.
+
+The DENY rule does not affect the last rule for "refs/*", since that
+has a different ref pattern and a different group. If group B is a
+superset of group A, the last rule will still grant group A access to
+"refs/a".
+
+
+==== Double use of exclusive
+
+An 'exclusive' permission is evaluated both during BLOCK processing
+and during ALLOW/DENY: when looking BLOCK, 'exclusive' stops the
+search downward, while the same permission in the ALLOW/DENY
+processing will stop looking upward for further rule matches
+
+==== Force permission
+
+The 'force' setting may be set on ALLOW and BLOCK rules. In the case
+of ALLOW, the 'force' option makes the permission stronger (allowing
+both forced and unforced actions). For BLOCK, the 'force' option makes
+it weaker (the BLOCK with 'force' only blocks forced actions).
+
+
+=== Labels
+
+Labels use the same mechanism, with the following observations:
+
+* The 'force' setting has no effect on label ranges.
+
+* BLOCK specifies the values that a group cannot vote, eg.
+----
+ label-Code-Review = block -2..+2 group Anonymous Users
+----
+prevents all users from voting -2 or +2.
+
+* DENY works for votes too, with the same caveats
+
+* The blocked vote range is the union of the all the blocked vote
+ranges across projects, so in
+----
+All-Projects: project.config
+ label-Code-Review = block -2..+1 group A
+
+Child-Project: project-config
+ label-Code-Review = block -1..+2 group A
+----
+members of group A cannot vote at all in the Child-Project.
+
+
+* The allowed vote range is the union of vote ranges allowed by all of
+the ALLOW rules. For example, in
+----
+ label-Code-Review = -2..+1 group A
+ label-Code-Review = -1..+2 group B
+----
+a user that is both in A and B can vote -2..2.
+
+
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 121fcad..59ed6ff 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -120,9 +120,10 @@
Votes that are not permitted for the user are silently ignored.
--label::
- Set a label by name to the value 'N'. Invalid votes (invalid label
- or invalid value) and votes that are not permitted for the user are
- silently ignored.
+ Set a label by name to the value 'N'. The ability to vote on all specified
+ labels is required. If the vote is invalid (invalid label or invalid name),
+ the vote is not permitted for the user, or the vote is on an outdated or
+ closed patch set, return an error instead of silently discarding the vote.
--tag::
-t::
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 38c3e0f..1b6d116 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -147,17 +147,6 @@
+
By default 1.
-[[audit]]
-=== Section audit
-
-[[audit.maskSensitiveData]]audit.maskSensitiveData::
-+
-If true, command parameters marked as sensitive are masked in audit logs.
-+
-This option only affects audit. Other means of logging will always be masked.
-+
-By default `false`.
-
[[auth]]
=== Section auth
@@ -1137,13 +1126,6 @@
+
Default is true.
-[[change.allowDrafts]]change.allowDrafts::
-+
-Allow drafts workflow. If set to false, drafts cannot be created,
-deleted or published.
-+
-Default is true.
-
[[change.api.allowedIdentifier]]change.api.allowedIdentifier::
+
Change identifier(s) that are allowed on the API. See
@@ -1311,39 +1293,16 @@
[[changeCleanup.startTime]]changeCleanup.startTime::
+
-Start time to define the first execution of the change cleanups.
-If the configured `'changeCleanup.interval'` is shorter than
-`'changeCleanup.startTime - now'` the start time will be preponed by
-the maximum integral multiple of `'changeCleanup.interval'` so that the
-start time is still in the future.
-+
-----
-<day of week> <hours>:<minutes>
-or
-<hours>:<minutes>
-
-<day of week> : Mon, Tue, Wed, Thu, Fri, Sat, Sun
-<hours> : 00-23
-<minutes> : 0-59
-----
-
+The link:#schedule-configuration-startTime[start time] for running
+change cleanups.
[[changeCleanup.interval]]changeCleanup.interval::
+
-Interval for periodic repetition of triggering the change cleanups.
-The interval must be larger than zero. The following suffixes are supported
-to define the time unit for the interval:
-+
-* `s, sec, second, seconds`
-* `m, min, minute, minutes`
-* `h, hr, hour, hours`
-* `d, day, days`
-* `w, week, weeks` (`1 week` is treated as `7 days`)
-* `mon, month, months` (`1 month` is treated as `30 days`)
-* `y, year, years` (`1 year` is treated as `365 days`)
+The link:#schedule-configuration-interval[interval] for running
+change cleanups.
-link:#schedule-examples[Schedule examples] can be found in the
-link:#gc[gc] section.
+link:#schedule-configuration-examples[Schedule examples] can be found
+in the link:#schedule-configuration[Schedule Configuration] section.
[[commentlink]]
=== Section commentlink
@@ -2013,59 +1972,16 @@
[[gc.startTime]]gc.startTime::
+
-Start time to define the first execution of the git garbage collection.
-If the configured `'gc.interval'` is shorter than `'gc.startTime - now'`
-the start time will be preponed by the maximum integral multiple of
-`'gc.interval'` so that the start time is still in the future.
-+
-----
-<day of week> <hours>:<minutes>
-or
-<hours>:<minutes>
-
-<day of week> : Mon, Tue, Wed, Thu, Fri, Sat, Sun
-<hours> : 00-23
-<minutes> : 0-59
-----
-
+The link:#schedule-configuration-startTime[start time] for running the
+git garbage collection.
[[gc.interval]]gc.interval::
+
-Interval for periodic repetition of triggering the git garbage collection.
-The interval must be larger than zero. The following suffixes are supported
-to define the time unit for the interval:
-+
-* `s, sec, second, seconds`
-* `m, min, minute, minutes`
-* `h, hr, hour, hours`
-* `d, day, days`
-* `w, week, weeks` (`1 week` is treated as `7 days`)
-* `mon, month, months` (`1 month` is treated as `30 days`)
-* `y, year, years` (`1 year` is treated as `365 days`)
+The link:#schedule-configuration-interval[interval] for running the
+git garbage collection.
-[[schedule-examples]]
-Examples::
-+
-----
-gc.startTime = Fri 10:30
-gc.interval = 2 day
-----
-+
-Assuming the server is started on Mon 7:00 -> `'startTime - now = 4 days 3:30 hours'`.
-This is larger than the interval hence prepone the start time
-by the maximum integral multiple of the interval so that start
-time is still in the future, i.e. prepone by 4 days. This yields
-a start time of Mon 10:30, next executions are Wed 10:30, Fri 10:30
-etc.
-+
-----
-gc.startTime = 6:00
-gc.interval = 1 day
-----
-+
-Assuming the server is started on Mon 7:00 this yields the first run on next Tuesday
-at 6:00 and a repetition interval of 1 day.
-
+link:#schedule-configuration-examples[Schedule examples] can be found
+in the link:#schedule-configuration[Schedule Configuration] section.
[[gerrit]]
=== Section gerrit
@@ -2833,6 +2749,62 @@
+
Defaults to true.
+[[index.scheduledIndexer]]
+==== Subsection index.scheduledIndexer
+
+This section configures periodic indexing. Periodic indexing is
+intended to run only on slaves and only updates the group index.
+Replication to slaves happens on Git level so that Gerrit is not aware
+of incoming replication events. But slaves need an updated group index
+to resolve memberships of users for ACL validation. To keep the group
+index in slaves up-to-date the Gerrit slave periodically scans the
+group refs in the All-Users repository to reindex groups if they are
+stale.
+
+The scheduled reindexer is not able to detect group deletions that
+happened while the slave was offline, but since group deletions are not
+supported this should never happen. If nevertheless groups refs were
+deleted while a slave was offline a full offline link:pgm-reindex.html[
+reindex] must be performed.
+
+This section is only used if Gerrit runs in slave mode, otherwise it is
+ignored.
+
+[[index.scheduledIndexer.runOnStartup]]index.scheduledIndexer.runOnStartup::
++
+Whether the scheduled indexer should run once immediately on startup.
+If set to `true` the slave startup is blocked until all stale groups
+were reindexed. Enabling this allows to prevent that slaves that were
+offline for a longer period of time run with outdated group information
+until the first scheduled indexing is done.
++
+Defaults to `true`.
+
+[[index.scheduledIndexer.enabled]]index.scheduledIndexer.enabled::
++
+Whether the scheduled indexer is enabled. If the scheduled indexer is
+disabled you must implement other means to keep the group index for the
+slave up-to-date (e.g. by using ElasticSearch for the indexes).
++
+Defaults to `true`.
+
+[[index.scheduledIndexer.startTime]]index.scheduledIndexer.startTime::
++
+The link:#schedule-configuration-startTime[start time] for running
+the scheduled indexer.
++
+Defaults to `00:00`.
+
+[[index.scheduledIndexer.interval]]index.scheduledIndexer.interval::
++
+The link:#schedule-configuration-interval[interval] for running
+the scheduled indexer.
++
+Defaults to `5m`.
+
+link:#schedule-configuration-examples[Schedule examples] can be found
+in the link:#schedule-configuration[Schedule Configuration] section.
+
==== Lucene configuration
Open and closed changes are indexed in separate indexes named
@@ -4737,34 +4709,16 @@
[[accountDeactivation.startTime]]accountDeactivation.startTime::
+
-Start time to define the first execution of account deactivations.
-If the configured `'accountDeactivation.interval'` is shorter than `'accountDeactivation.startTime - now'`
-the start time will be preponed by the maximum integral multiple of
-`'accountDeactivation.interval'` so that the start time is still in the future.
-+
-----
-<day of week> <hours>:<minutes>
-or
-<hours>:<minutes>
-
-<day of week> : Mon, Tue, Wed, Thu, Fri, Sat, Sun
-<hours> : 00-23
-<minutes> : 0-59
-----
+The link:#schedule-configuration-startTime[start time] for running
+account deactivations.
[[accountDeactivation.interval]]accountDeactivation.interval::
+
-Interval for periodic repetition of triggering account deactivation sweeps.
-The interval must be larger than zero. The following suffixes are supported
-to define the time unit for the interval:
-+
-* `s, sec, second, seconds`
-* `m, min, minute, minutes`
-* `h, hr, hour, hours`
-* `d, day, days`
-* `w, week, weeks` (`1 week` is treated as `7 days`)
-* `mon, month, months` (`1 month` is treated as `30 days`)
-* `y, year, years` (`1 year` is treated as `365 days`)
+The link:#schedule-configuration-interval[interval] for running
+account deactivations.
+
+link:#schedule-configuration-examples[Schedule examples] can be found
+in the link:#schedule-configuration[Schedule Configuration] section.
[[urlAlias]]
=== Section urlAlias
@@ -4872,6 +4826,89 @@
+
By default "Name of user not set" is used.
+[[schedule-configuration]]
+=== Schedule Configuration
+
+Schedule configurations are used for running periodic background jobs.
+
+A schedule configuration consists of two parameters:
+
+[[schedule-configuration-interval]]
+* `interval`:
+Interval for running the periodic background job. The interval must be
+larger than zero. The following suffixes are supported to define the
+time unit for the interval:
+** `s`, `sec`, `second`, `seconds`
+** `m`, `min`, `minute`, `minutes`
+** `h`, `hr`, `hour`, `hours`
+** `d`, `day`, `days`
+** `w`, `week`, `weeks` (`1 week` is treated as `7 days`)
+** `mon`, `month`, `months` (`1 month` is treated as `30 days`)
+** `y`, `year`, `years` (`1 year` is treated as `365 days`)
+
+[[schedule-configuration-startTime]]
+* `startTime`:
+The start time defines the first execution of the periodic background
+job. If the configured `interval` is shorter than `startTime - now` the
+start time will be preponed by the maximum integral multiple of
+`interval` so that the start time is still in the future. `startTime`
+must have one of the following formats:
+
+** `<day of week> <hours>:<minutes>`
+** `<hours>:<minutes>`
+
++
+The placeholders can have the following values:
+
+*** `<day of week>`:
+`Mon`, `Tue`, `Wed`, `Thu`, `Fri`, `Sat`, `Sun`
+*** `<hours>`:
+`00`-`23`
+*** `<minutes>`:
+`00`-`59`
+
++
+The time zone cannot be specified but is always the system default
+time zone.
+
+The section (and optionally the subsection) in which the `interval` and
+`startTime` keys must be set depends on the background job for which a
+schedule should be configured. E.g. for the change cleanup job the keys
+must be set in the link:#changeCleanup[changeCleanup] section:
+
+----
+ [changeCleanup]
+ startTime = Fri 10:30
+ interval = 2 days
+----
+
+[[schedule-configuration-examples]]
+Examples for a schedule configuration:
+
+* Example 1:
++
+----
+ startTime = Fri 10:30
+ interval = 2 days
+----
++
+Assuming that the server is started on `Mon 07:00` then
+`startTime - now` is `4 days 3:30 hours`. This is larger than the
+interval hence the start time is preponed by the maximum integral
+multiple of the interval so that start time is still in the future,
+i.e. preponed by 4 days. This yields a start time of `Mon 10:30`, next
+executions are `Wed 10:30`, `Fri 10:30`. etc.
+
+* Example 2:
++
+----
+ startTime = 06:00
+ interval = 1 day
+----
++
+Assuming that the server is started on `Mon 07:00` then this yields the
+first run on Tuesday at 06:00 and a repetition interval of 1 day.
+
[[secure.config]]
== File `etc/secure.config`
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index 16a3a93..185feb4 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -39,6 +39,14 @@
The core plugins are developed and maintained by the Gerrit maintainers
and the Gerrit community.
+[[codemirror-editor]]
+=== codemirror-editor
+
+CodeMirror plugin for polygerrit.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/codemirror-editor[
+Project] |
+
[[commit-message-length-validator]]
=== commit-message-length-validator
@@ -53,16 +61,6 @@
link:https://gerrit.googlesource.com/plugins/commit-message-length-validator/+doc/master/src/main/resources/Documentation/config.md[
Configuration]
-[[cookbook-plugin]]
-=== cookbook-plugin
-
-Sample plugin to demonstrate features of Gerrit's plugin API.
-
-link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/cookbook-plugin[
-Project] |
-link:https://gerrit.googlesource.com/plugins/cookbook-plugin/+doc/master/src/main/resources/Documentation/about.md[
-Documentation]
-
[[download-commands]]
=== download-commands
@@ -347,6 +345,16 @@
link:https://gerrit.googlesource.com/plugins/its-jira/+doc/master/src/main/resources/Documentation/config.md[
Configuration]
+[[its-phabricator]]
+==== its-phabricator
+
+Plugin to integrate with Phabricator.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/its-phabricator[
+Project] |
+link:https://gerrit.googlesource.com/plugins/its-phabricator/+doc/master/src/main/resources/Documentation/config.md[
+Configuration]
+
[[its-rtc]]
==== its-rtc
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index b482de1..f6cdc06 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -363,18 +363,14 @@
=== Cleaning The download cache
The cache for downloaded artifacts is located in
-`~/.gerritcodereview/buck-cache/downloaded-artifacts`.
+`~/.gerritcodereview/bazel-cache/downloaded-artifacts`.
If you really do need to clean the download cache manually, then:
----
- rm -rf ~/.gerritcodereview/buck-cache/downloaded-artifacts
+ rm -rf ~/.gerritcodereview/bazel-cache/downloaded-artifacts
----
-[NOTE] When building with Bazel the artifacts are still cached in
-`~/.gerritcodereview/buck-cache/downloaded-artifacts`. This allows Bazel to
-make use of libraries that were previously downloaded by Buck.
-
[[local-action-cache]]
To accelerate builds, local action cache can be activated. Note, that this
diff --git a/Documentation/dev-build-plugins.txt b/Documentation/dev-build-plugins.txt
index 5dacd71..072c22c 100644
--- a/Documentation/dev-build-plugins.txt
+++ b/Documentation/dev-build-plugins.txt
@@ -121,11 +121,24 @@
]
----
+If the plugin(s) being bundled in the release have external dependencies, include them
+in `plugins/external_plugin_deps`. You should alias `external_plugin_deps()` so it
+can be imported for multiple plugins. For example:
+
+----
+load(":my-plugin/external_plugin_deps.bzl", my_plugin="external_plugin_deps")
+load(":my-other-plugin/external_plugin_deps.bzl", my_other_plugin="external_plugin_deps")
+
+def external_plugin_deps():
+ my_plugin()
+ my_other_plugin()
+----
+
[NOTE]
-Since `tools/bzl/plugins.bzl` is part of Gerrit's source code and the version
-of the war is based on the state of the git repository that is built; you should
-commit this change before building, otherwise the version will be marked as
-'dirty'.
+Since `tools/bzl/plugins.bzl` and `plugins/external_plugin_deps.bzl` are part of
+Gerrit's source code and the version of the war is based on the state of the git
+repository that is built; you should commit this change before building, otherwise
+the version will be marked as 'dirty'.
== Bazel standalone driven
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 4a64e68..90b8521 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2659,15 +2659,18 @@
Gerrit provides an extension point that enables Plugins to rank
the list of reviewer suggestion a user receives upon clicking "Add Reviewer" on
the change screen.
+
Gerrit supports both a default suggestion that appears when the user has not yet
typed anything and a filtered suggestion that is shown as the user starts
typing.
-Plugins receive a candidate list and can return a Set of suggested reviewers
-containing the Account.Id and a score for each reviewer.
-The candidate list is non-binding and plugins can choose to return reviewers not
-initially contained in the candidate list.
-Server administrators can configure the overall weight of each plugin using the
-weight config parameter on [addreviewer "<pluginName-exportName>"].
+
+Plugins receive a candidate list and can return a `Set` of suggested reviewers
+containing the `Account.Id` and a score for each reviewer. The candidate list is
+non-binding and plugins can choose to return reviewers not initially contained in
+the candidate list.
+
+Server administrators can configure the overall weight of each plugin by setting
+the `addreviewer.pluginName-exportName.weight` value in `gerrit.config`.
[source, java]
----
@@ -2706,7 +2709,7 @@
import com.google.gerrit.server.mail.receive.MailMessage;
public class MyPlugin implements MailFilter {
- boolean shouldProcessMessage(MailMessage message) {
+ public boolean shouldProcessMessage(MailMessage message) {
// Implement your filter logic here
return true;
}
diff --git a/Documentation/dev-polygerrit.txt b/Documentation/dev-polygerrit.txt
index 7898ae9..79049fc 100644
--- a/Documentation/dev-polygerrit.txt
+++ b/Documentation/dev-polygerrit.txt
@@ -1,13 +1,7 @@
= PolyGerrit - GUI
[IMPORTANT]
-PolyGerrit is still a beta feature...
-
-Missing features in PolyGerrit:
-
-- Inline Edit
-
-- And many more features missing.
+PolyGerrit is still a beta feature. Some features may be missing.
== Configuring
diff --git a/Documentation/dev-release-subproject.txt b/Documentation/dev-release-subproject.txt
index fcafea5..4886849 100644
--- a/Documentation/dev-release-subproject.txt
+++ b/Documentation/dev-release-subproject.txt
@@ -32,7 +32,7 @@
----
* Change the `id`, `bin_sha1`, and `src_sha1` values in the `maven_jar`
-for the subproject in `/lib/BUCK` to the `SNAPSHOT` version.
+for the subproject in `/WORKSPACE` to the `SNAPSHOT` version.
+
When Gerrit gets released, a release of the subproject has to be done
and Gerrit has to reference the released subproject version.
diff --git a/Documentation/note-db.txt b/Documentation/note-db.txt
index 28066bc..fd2bef37 100644
--- a/Documentation/note-db.txt
+++ b/Documentation/note-db.txt
@@ -104,6 +104,11 @@
Once started, it is safe to cancel and restart the migration process, or to
switch to the online process.
+[NOTE]
+Migration requires a heap size comparable to running a Gerrit server. If you
+normally run `gerrit.war daemon` with an `-Xmx` flag, pass that to the migration
+tool as well.
+
*Advantages*
* Much faster than online; can use all available CPUs, since no live traffic
diff --git a/Documentation/pgm-MigrateAccountPatchReviewDb.txt b/Documentation/pgm-MigrateAccountPatchReviewDb.txt
index 5718a8a..c8ab193 100644
--- a/Documentation/pgm-MigrateAccountPatchReviewDb.txt
+++ b/Documentation/pgm-MigrateAccountPatchReviewDb.txt
@@ -27,6 +27,12 @@
* Migrate data using this command
* Start Gerrit
+[NOTE]
+When using MySQL, the file_name column length in the account_patch_reviews table will be shortened
+from the standard 4096 characters down to 255 characters. This is due to a
+link:https://dev.mysql.com/doc/refman/5.7/en/innodb-restrictions.html[MySQL limitation]
+on the max size of 767 bytes for each column in an index.
+
== OPTIONS
-d::
diff --git a/Documentation/prolog-change-facts.txt b/Documentation/prolog-change-facts.txt
index a62e0c3..2fb13e9 100644
--- a/Documentation/prolog-change-facts.txt
+++ b/Documentation/prolog-change-facts.txt
@@ -51,13 +51,6 @@
|`commit_stats/3` |`commit_stats(5,20,50).`
|Number of files modified, number of insertions and the number of deletions.
-.4+|`current_user/1` |`current_user(user(1000000)).`
- .4+|Current user as one of the four given possibilities
-
- |`current_user(user(anonymous)).`
- |`current_user(user(peer_daemon)).`
- |`current_user(user(replication)).`
-
|`pure_revert/1` |`pure_revert(1).`
|link:rest-api-changes.html#get-pure-revert[Pure revert] as integer atom (1 if
the change is a pure revert, 0 otherwise)
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index 7561286..386e2d6 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -977,30 +977,7 @@
add_apprentice_master(S, S).
----
-=== Example 15: Only allow Author to submit change
-This example adds a new needed category `Only-Author-Can-Submit` for any user
-that is not the author of the patch. This effectively blocks all users except
-the author from submitting the change. This could result in an impossible
-situation if the author does not have permissions for submitting the change.
-
-`rules.pl`
-[source,prolog]
-----
-submit_rule(S) :-
- gerrit:default_submit(In),
- In =.. [submit | Ls],
- only_allow_author_to_submit(Ls, R),
- S =.. [submit | R].
-
-only_allow_author_to_submit(S, S) :-
- gerrit:commit_author(Id),
- gerrit:current_user(Id),
- !.
-
-only_allow_author_to_submit(S1, [label('Only-Author-Can-Submit', need(_)) | S1]).
-----
-
-=== Example 16: Make change submittable if all comments have been resolved
+=== Example 15: Make change submittable if all comments have been resolved
In this example we will use the `unresolved_comments_count` fact about a
change. Our goal is to block the submission of any change with some
unresolved comments. Basically, it can be achieved by the following rules:
@@ -1050,7 +1027,7 @@
indicate to the user that all the comments have to be resolved for the
change to become submittable.
-=== Example 17: Make change submittable if it is a pure revert
+=== Example 16: Make change submittable if it is a pure revert
In this example we will use the `pure_revert` fact about a
change. Our goal is to block the submission of any change that is not a
pure revert. Basically, it can be achieved by the following rules:
diff --git a/Documentation/rest-api-access.txt b/Documentation/rest-api-access.txt
index 5a49f38..65a15ca 100644
--- a/Documentation/rest-api-access.txt
+++ b/Documentation/rest-api-access.txt
@@ -402,10 +402,11 @@
|`config_visible` |not set if `false`|
Whether the calling user can see the `refs/meta/config` branch of the
project.
-|`groups` |A map of group UUID to
-link:rest-api-groups.html#group-info[GroupInfo] objects, describing
-the group UUIDs used in the `local` map. Groups that are not visible
-are omitted from the `groups` map.
+|`groups` ||A map of group UUID to
+link:rest-api-groups.html#group-info[GroupInfo] objects, with names and
+URLs for the group UUIDs used in the `local` map.
+This will include names for groups that might
+be invisible to the caller.
|`configWebLinks` ||
A list of URLs that display the history of the configuration file
governing this project's access rights.
diff --git a/WORKSPACE b/WORKSPACE
index 6044953..ac0ffd2 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -11,13 +11,12 @@
urls = ["https://github.com/bazelbuild/bazel-skylib/archive/2169ae1c374aab4a09aa90e65efe1a3aad4e279b.tar.gz"],
)
-# davido's fork with https://github.com/bazelbuild/rules_closure/issues/248 included
-# to avoid workspace name mismatch warning
+# davido's fork with https://github.com/bazelbuild/rules_closure/pull/235 included
http_archive(
name = "io_bazel_rules_closure",
- sha256 = "83ea206d5fb9178f5faab7ffb173018c2908df5c73bdb0327e3272442ccce661",
- strip_prefix = "rules_closure-0.8.0",
- url = "https://github.com/davido/rules_closure/archive/0.8.0.tar.gz",
+ sha256 = "314e4eb701696e267cb911609e2e333e321fe641981a33144f460068ff4e1af3",
+ strip_prefix = "rules_closure-0.11.0",
+ url = "https://github.com/davido/rules_closure/archive/0.11.0.tar.gz",
)
# File is specific to Polymer and copied from the Closure Github -- should be
@@ -600,8 +599,8 @@
maven_jar(
name = "dropwizard_core",
- artifact = "io.dropwizard.metrics:metrics-core:3.2.5",
- sha1 = "ea2316646e9787c5b2d14ca97f4ef7ad5c6b94e9",
+ artifact = "io.dropwizard.metrics:metrics-core:4.0.2",
+ sha1 = "ec9878842d510cabd6bd6a9da1bebae1ae0cd199",
)
# When updading Bouncy Castle, also update it in bazlets.
@@ -1100,6 +1099,13 @@
)
bower_archive(
+ name = "paper-tabs",
+ package = "polymerelements/paper-tabs",
+ sha1 = "b6dd2fbd7ee887534334057a29eb545b940fc5cf",
+ version = "2.0.0",
+)
+
+bower_archive(
name = "iron-icon",
package = "polymerelements/iron-icon",
sha1 = "7da49a0d33cd56017740e0dbcf41d2b71532023f",
@@ -1151,8 +1157,8 @@
bower_archive(
name = "polymer",
package = "polymer/polymer",
- sha1 = "62ce80a5079c1b97f6c5c6ebf6b350e741b18b9c",
- version = "1.11.0",
+ sha1 = "158443ab05ade5e2cdc24ebc01f1deef9aebac1b",
+ version = "1.11.3",
)
bower_archive(
diff --git a/antlr3/BUILD b/antlr3/BUILD
index 0766194..6d7102a 100644
--- a/antlr3/BUILD
+++ b/antlr3/BUILD
@@ -7,11 +7,11 @@
cmd = " && ".join([
"$(location //lib/antlr:antlr-tool) -o $$TMP $<",
"cd $$TMP",
- "zip $$ROOT/$@ $$(find . -type f )",
+ "find . -exec touch -t 198001010000 '{}' ';'",
+ "zip -q $$ROOT/$@ $$(find . -type f)",
]),
tools = [
"//lib/antlr:antlr-tool",
- "@bazel_tools//tools/zip:zipper",
],
visibility = ["//visibility:public"],
)
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py
index 8d1c326..3501b8b 100755
--- a/contrib/abandon_stale.py
+++ b/contrib/abandon_stale.py
@@ -148,7 +148,7 @@
if options.testmode:
query_terms = ["status:new", "owner:self", "topic:test-abandon"]
else:
- query_terms = ["status:new", "age:%s" % options.age]
+ query_terms = ["status:new", "-is:wip", "age:%s" % options.age]
if options.branches:
query_terms += ["branch:%s" % b for b in options.branches]
elif options.exclude_branches:
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
index dcd1cf1..d3274e6 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ServerInfo.java
@@ -60,8 +60,6 @@
protected ServerInfo() {}
public static class ChangeConfigInfo extends JavaScriptObject {
- public final native boolean allowDrafts() /*-{ return this.allow_drafts || false; }-*/;
-
public final native boolean allowBlame() /*-{ return this.allow_blame || false; }-*/;
public final native int largeChange() /*-{ return this.large_change || 0; }-*/;
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 5755322..77dc8ea 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -106,6 +106,7 @@
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.index.group.GroupIndexer;
import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.mail.send.EmailHeader;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
@@ -268,6 +269,7 @@
@Inject private Provider<AnonymousUser> anonymousUser;
@Inject private SchemaFactory<ReviewDb> reviewDbProvider;
@Inject private Groups groups;
+ @Inject private GroupIndexer groupIndexer;
private ProjectResetter resetter;
private List<Repository> toClose;
@@ -343,6 +345,13 @@
initSsh();
}
+ private void reindexAllGroups() throws OrmException, IOException, ConfigInvalidException {
+ Iterable<GroupReference> allGroups = groups.getAllGroupReferences(db)::iterator;
+ for (GroupReference group : allGroups) {
+ groupIndexer.index(group.getUUID());
+ }
+ }
+
protected static Config submitWholeTopicEnabledConfig() {
Config cfg = new Config();
cfg.setBoolean("change", null, "submitWholeTopic", true);
@@ -394,10 +403,7 @@
// later on. As test indexes are non-permanent, closing an instance and opening another one
// removes all indexed data.
// As a workaround, we simply reindex all available groups here.
- Iterable<GroupReference> allGroups = groups.getAllGroupReferences(db)::iterator;
- for (GroupReference group : allGroups) {
- groupCache.onCreateGroup(group.getUUID());
- }
+ reindexAllGroups();
admin = accountCreator.admin();
user = accountCreator.user();
@@ -1506,7 +1512,7 @@
assertThat(m.rcpt()).containsExactly(expected);
assertThat(((EmailHeader.AddressList) m.headers().get("To")).getAddressList())
.containsExactly(expected);
- assertThat(m.headers().get("CC").isEmpty()).isTrue();
+ assertThat(m.headers().get("Cc").isEmpty()).isTrue();
}
protected void assertNotifyCc(TestAccount expected) {
@@ -1518,7 +1524,7 @@
Message m = sender.getMessages().get(0);
assertThat(m.rcpt()).containsExactly(expected);
assertThat(m.headers().get("To").isEmpty()).isTrue();
- assertThat(((EmailHeader.AddressList) m.headers().get("CC")).getAddressList())
+ assertThat(((EmailHeader.AddressList) m.headers().get("Cc")).getAddressList())
.containsExactly(expected);
}
@@ -1527,7 +1533,7 @@
Message m = sender.getMessages().get(0);
assertThat(m.rcpt()).containsExactly(expected.emailAddress);
assertThat(m.headers().get("To").isEmpty()).isTrue();
- assertThat(m.headers().get("CC").isEmpty()).isTrue();
+ assertThat(m.headers().get("Cc").isEmpty()).isTrue();
}
protected interface ProjectWatchInfoConfiguration {
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index 8333005..086cacb 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -113,7 +113,7 @@
}
recipients = new HashMap<>();
recipients.put(TO, parseAddresses(message, "To"));
- recipients.put(CC, parseAddresses(message, "CC"));
+ recipients.put(CC, parseAddresses(message, "Cc"));
recipients.put(
BCC,
message
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 5262b74..37ef20a 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -317,7 +317,7 @@
daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
daemon.setAdditionalSysModuleForTesting(testSysModule);
daemon.setEnableSshd(desc.useSsh());
- daemon.setSlave(baseConfig.getBoolean("container", "slave", false));
+ daemon.setSlave(isSlave(baseConfig));
if (desc.memory()) {
checkArgument(additionalArgs.length == 0, "cannot pass args to in-memory server");
@@ -345,7 +345,7 @@
cfg.setBoolean("index", "lucene", "testInmemory", true);
cfg.setString("gitweb", null, "cgi", "");
daemon.setEnableHttpd(desc.httpd());
- daemon.setLuceneModule(LuceneIndexModule.singleVersionAllLatest(0));
+ daemon.setLuceneModule(LuceneIndexModule.singleVersionAllLatest(0, isSlave(baseConfig)));
daemon.setDatabaseForTesting(
ImmutableList.<Module>of(
new InMemoryTestingDatabaseModule(
@@ -354,6 +354,10 @@
return new GerritServer(desc, null, createTestInjector(daemon), daemon, null);
}
+ private static boolean isSlave(Config baseConfig) {
+ return baseConfig.getBoolean("container", "slave", false);
+ }
+
private static GerritServer startOnDisk(
Description desc,
Path site,
diff --git a/java/com/google/gerrit/acceptance/InProcessProtocol.java b/java/com/google/gerrit/acceptance/InProcessProtocol.java
index 629c6bd..24d4b6b 100644
--- a/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -30,13 +30,14 @@
import com.google.gerrit.server.RequestCleanup;
import com.google.gerrit.server.config.GerritRequestModule;
import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
+import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
import com.google.gerrit.server.git.ReceivePackInitializer;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.git.UploadPackInitializer;
-import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.project.ProjectCache;
@@ -208,7 +209,6 @@
private static class Upload implements UploadPackFactory<Context> {
private final Provider<CurrentUser> userProvider;
- private final VisibleRefFilter.Factory refFilterFactory;
private final TransferConfig transferConfig;
private final DynamicSet<UploadPackInitializer> uploadPackInitializers;
private final DynamicSet<PreUploadHook> preUploadHooks;
@@ -220,7 +220,6 @@
@Inject
Upload(
Provider<CurrentUser> userProvider,
- VisibleRefFilter.Factory refFilterFactory,
TransferConfig transferConfig,
DynamicSet<UploadPackInitializer> uploadPackInitializers,
DynamicSet<PreUploadHook> preUploadHooks,
@@ -229,7 +228,6 @@
ProjectCache projectCache,
PermissionBackend permissionBackend) {
this.userProvider = userProvider;
- this.refFilterFactory = refFilterFactory;
this.transferConfig = transferConfig;
this.uploadPackInitializers = uploadPackInitializers;
this.preUploadHooks = preUploadHooks;
@@ -248,11 +246,9 @@
threadContext.setContext(req);
current.set(req);
+ PermissionBackend.ForProject perm = permissionBackend.user(userProvider).project(req.project);
try {
- permissionBackend
- .user(userProvider)
- .project(req.project)
- .check(ProjectPermission.RUN_UPLOAD_PACK);
+ perm.check(ProjectPermission.RUN_UPLOAD_PACK);
} catch (AuthException e) {
throw new ServiceNotAuthorizedException();
} catch (PermissionBackendException e) {
@@ -271,7 +267,7 @@
UploadPack up = new UploadPack(repo);
up.setPackConfig(transferConfig.getPackConfig());
up.setTimeout(transferConfig.getTimeout());
- up.setAdvertiseRefsHook(refFilterFactory.create(projectState, repo));
+ up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
List<PreUploadHook> hooks = Lists.newArrayList(preUploadHooks);
hooks.add(uploadValidatorsFactory.create(projectState.getProject(), repo, "localhost-test"));
up.setPreUploadHook(PreUploadHookChain.newChain(hooks));
diff --git a/java/com/google/gerrit/common/Common.gwt.xml b/java/com/google/gerrit/common/Common.gwt.xml
index fede665..56bbb84 100644
--- a/java/com/google/gerrit/common/Common.gwt.xml
+++ b/java/com/google/gerrit/common/Common.gwt.xml
@@ -18,6 +18,7 @@
<inherits name='com.google.gwtjsonrpc.GWTJSONRPC'/>
<inherits name="com.google.gwt.logging.Logging"/>
<source path="">
+ <exclude name='**/testing/**/*.java'/>
<include name='**/*.java'/>
</source>
</module>
diff --git a/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java b/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
index 538565a..e8fa896 100644
--- a/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
+++ b/java/com/google/gerrit/common/SiteLibraryLoaderUtil.java
@@ -15,6 +15,7 @@
package com.google.gerrit.common;
import static com.google.gerrit.common.FileUtil.lastModified;
+import static java.util.stream.Collectors.joining;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.collect.ComparisonChain;
@@ -35,12 +36,18 @@
public static void loadSiteLib(Path libdir) {
try {
- IoUtil.loadJARs(listJars(libdir));
+ List<Path> jars = listJars(libdir);
+ IoUtil.loadJARs(jars);
+ log.debug("Loaded site libraries: {}", jarList(jars));
} catch (IOException e) {
log.error("Error scanning lib directory " + libdir, e);
}
}
+ private static String jarList(List<Path> jars) {
+ return jars.stream().map(p -> p.getFileName().toString()).collect(joining(","));
+ }
+
public static List<Path> listJars(Path dir) throws IOException {
DirectoryStream.Filter<Path> filter =
new DirectoryStream.Filter<Path>() {
diff --git a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
index b689254..1988d66 100644
--- a/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
+++ b/java/com/google/gerrit/common/data/testing/GroupReferenceSubject.java
@@ -16,7 +16,6 @@
import static com.google.common.truth.Truth.assertAbout;
-import com.google.common.annotations.GwtIncompatible;
import com.google.common.truth.ComparableSubject;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.StringSubject;
@@ -25,7 +24,6 @@
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.AccountGroup;
-@GwtIncompatible("Unemulated com.google.gerrit.common.data.testing.GroupReferenceSubject")
public class GroupReferenceSubject extends Subject<GroupReferenceSubject, GroupReference> {
public static GroupReferenceSubject assertThat(GroupReference group) {
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index b21d3df..0a9f0ec 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -290,6 +290,7 @@
if (source.get(ChangeField.REVIEWER.getName()) != null) {
cd.setReviewers(
ChangeField.parseReviewerFieldValues(
+ cd.getId(),
FluentIterable.from(source.get(ChangeField.REVIEWER.getName()).getAsJsonArray())
.transform(JsonElement::getAsString)));
} else if (fields.contains(ChangeField.REVIEWER.getName())) {
@@ -300,6 +301,7 @@
if (source.get(ChangeField.REVIEWER_BY_EMAIL.getName()) != null) {
cd.setReviewersByEmail(
ChangeField.parseReviewerByEmailFieldValues(
+ cd.getId(),
FluentIterable.from(
source.get(ChangeField.REVIEWER_BY_EMAIL.getName()).getAsJsonArray())
.transform(JsonElement::getAsString)));
@@ -311,6 +313,7 @@
if (source.get(ChangeField.PENDING_REVIEWER.getName()) != null) {
cd.setPendingReviewers(
ChangeField.parseReviewerFieldValues(
+ cd.getId(),
FluentIterable.from(
source.get(ChangeField.PENDING_REVIEWER.getName()).getAsJsonArray())
.transform(JsonElement::getAsString)));
@@ -322,6 +325,7 @@
if (source.get(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName()) != null) {
cd.setPendingReviewersByEmail(
ChangeField.parseReviewerByEmailFieldValues(
+ cd.getId(),
FluentIterable.from(
source.get(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName()).getAsJsonArray())
.transform(JsonElement::getAsString)));
diff --git a/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java b/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
index 2d04e11..441653a 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.Schema;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.index.IndexModule;
@@ -36,52 +37,60 @@
public class ElasticIndexModule extends AbstractModule {
public static ElasticIndexModule singleVersionWithExplicitVersions(
- Map<String, Integer> versions, int threads) {
- return new ElasticIndexModule(versions, threads, false);
+ Map<String, Integer> versions, int threads, boolean slave) {
+ return new ElasticIndexModule(versions, threads, false, slave);
}
- public static ElasticIndexModule latestVersionWithOnlineUpgrade() {
- return new ElasticIndexModule(null, 0, true);
+ public static ElasticIndexModule latestVersionWithOnlineUpgrade(boolean slave) {
+ return new ElasticIndexModule(null, 0, true, slave);
}
- public static ElasticIndexModule latestVersionWithoutOnlineUpgrade() {
- return new ElasticIndexModule(null, 0, false);
+ public static ElasticIndexModule latestVersionWithoutOnlineUpgrade(boolean slave) {
+ 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) {
+ 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() {
- install(
- new FactoryModuleBuilder()
- .implement(AccountIndex.class, ElasticAccountIndex.class)
- .build(AccountIndex.Factory.class));
- install(
- new FactoryModuleBuilder()
- .implement(ChangeIndex.class, ElasticChangeIndex.class)
- .build(ChangeIndex.Factory.class));
+ 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 FactoryModuleBuilder()
- .implement(ProjectIndex.class, ElasticProjectIndex.class)
- .build(ProjectIndex.Factory.class));
- install(new IndexModule(threads));
+ install(new IndexModule(threads, slave));
if (singleVersions == null) {
install(new MultiVersionModule());
} else {
@@ -89,6 +98,11 @@
}
}
+ @SuppressWarnings("unused")
+ private static <T> T createDummyIndexFactory(Schema<?> schema) {
+ throw new UnsupportedOperationException();
+ }
+
@Provides
@Singleton
IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
diff --git a/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java b/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
index 9e02ae5..1e822e3 100644
--- a/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
@@ -17,7 +17,6 @@
public class ChangeConfigInfo {
public Boolean allowBlame;
public Boolean showAssigneeInChangesTable;
- public Boolean allowDrafts;
public Boolean disablePrivateChanges;
public int largeChange;
public String replyLabel;
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 4d472da..5bdd9ca 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -25,13 +25,14 @@
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.git.UploadPackInitializer;
-import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.project.ProjectCache;
@@ -238,18 +239,15 @@
}
static class UploadFilter implements Filter {
- private final VisibleRefFilter.Factory refFilterFactory;
private final UploadValidators.Factory uploadValidatorsFactory;
private final PermissionBackend permissionBackend;
private final Provider<CurrentUser> userProvider;
@Inject
UploadFilter(
- VisibleRefFilter.Factory refFilterFactory,
UploadValidators.Factory uploadValidatorsFactory,
PermissionBackend permissionBackend,
Provider<CurrentUser> userProvider) {
- this.refFilterFactory = refFilterFactory;
this.uploadValidatorsFactory = uploadValidatorsFactory;
this.permissionBackend = permissionBackend;
this.userProvider = userProvider;
@@ -262,12 +260,10 @@
Repository repo = ServletUtils.getRepository(request);
ProjectState state = (ProjectState) request.getAttribute(ATT_STATE);
UploadPack up = (UploadPack) request.getAttribute(ServletUtils.ATTRIBUTE_HANDLER);
-
+ PermissionBackend.ForProject perm =
+ permissionBackend.user(userProvider).project(state.getNameKey());
try {
- permissionBackend
- .user(userProvider)
- .project(state.getNameKey())
- .check(ProjectPermission.RUN_UPLOAD_PACK);
+ perm.check(ProjectPermission.RUN_UPLOAD_PACK);
} catch (AuthException e) {
GitSmartHttpTools.sendError(
(HttpServletRequest) request,
@@ -284,8 +280,7 @@
uploadValidatorsFactory.create(state.getProject(), repo, request.getRemoteHost());
up.setPreUploadHook(
PreUploadHookChain.newChain(Lists.newArrayList(up.getPreUploadHook(), uploadValidators)));
- up.setAdvertiseRefsHook(refFilterFactory.create(state, repo));
-
+ up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
next.doFilter(request, response);
}
diff --git a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index 86ed398..fd42e82 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -487,10 +487,15 @@
final StringBuilder rdr = new StringBuilder();
rdr.append(urlProvider.get(req));
+ String nextToken = Url.decode(token);
if (isNew && !token.startsWith(PageLinks.REGISTER + "/")) {
rdr.append('#' + PageLinks.REGISTER);
+ if (nextToken.startsWith("#")) {
+ // Need to strip the leading # off the token to fix registration page redirect
+ nextToken = nextToken.substring(1);
+ }
}
- rdr.append(Url.decode(token));
+ rdr.append(nextToken);
rsp.sendRedirect(rdr.toString());
}
diff --git a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
index 097f7e7..a498d12 100644
--- a/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
+++ b/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -32,6 +32,8 @@
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.Url;
@@ -71,6 +73,7 @@
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -458,7 +461,7 @@
private static Map<String, String> getParameters(HttpServletRequest req) {
final Map<String, String> params = new HashMap<>();
- for (String pair : req.getQueryString().split("[&;]")) {
+ for (String pair : Splitter.on(CharMatcher.anyOf("&;")).split(req.getQueryString())) {
final int eq = pair.indexOf('=');
if (0 < eq) {
String name = pair.substring(0, eq);
@@ -689,8 +692,8 @@
res.sendRedirect(value);
} else if ("Status".equalsIgnoreCase(key)) {
- final String[] token = value.split(" ");
- final int status = Integer.parseInt(token[0]);
+ final List<String> token = Splitter.on(' ').splitToList(value);
+ final int status = Integer.parseInt(token.get(0));
res.setStatus(status);
} else {
diff --git a/java/com/google/gerrit/httpd/init/WebAppInitializer.java b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
index fa114e6..d7dca11 100644
--- a/java/com/google/gerrit/httpd/init/WebAppInitializer.java
+++ b/java/com/google/gerrit/httpd/init/WebAppInitializer.java
@@ -392,9 +392,9 @@
private Module createIndexModule() {
switch (indexType) {
case LUCENE:
- return LuceneIndexModule.latestVersionWithOnlineUpgrade();
+ return LuceneIndexModule.latestVersionWithOnlineUpgrade(false);
case ELASTICSEARCH:
- return ElasticIndexModule.latestVersionWithOnlineUpgrade();
+ return ElasticIndexModule.latestVersionWithOnlineUpgrade(false);
default:
throw new IllegalStateException("unsupported index.type = " + indexType);
}
diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
index e1c094c..2cdca13 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -457,7 +457,7 @@
GuiceFilterRequestWrapper reqWrapper = new GuiceFilterRequestWrapper(req);
String path = pathInfo(req);
- // Special case assets during development that are built by Buck and not
+ // Special case assets during development that are built by Bazel and not
// served out of the source tree.
//
// In the war case, these are either inlined by vulcanize, or live under
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 5b24284..d070966 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -189,6 +189,8 @@
public static final String XD_METHOD = "$m";
private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks.
+ private static final String PLAIN_TEXT = "text/plain";
+ private static final Pattern TYPE_SPLIT_PATTERN = Pattern.compile("[ ,;][ ,;]*");
/**
* Garbage prefix inserted before JSON output to prevent XSSI.
@@ -302,7 +304,7 @@
if (isRead(req)) {
viewData = new ViewData(null, rc.list());
- } else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) {
+ } else if (rc instanceof AcceptsPost && isPost(req)) {
@SuppressWarnings("unchecked")
AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) rc;
viewData = new ViewData(null, ac.post(rsrc));
@@ -317,9 +319,7 @@
checkPreconditions(req);
}
} catch (ResourceNotFoundException e) {
- if (rc instanceof AcceptsCreate
- && path.isEmpty()
- && ("POST".equals(req.getMethod()) || "PUT".equals(req.getMethod()))) {
+ if (rc instanceof AcceptsCreate && path.isEmpty() && (isPost(req) || isPut(req))) {
@SuppressWarnings("unchecked")
AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) rc;
viewData = new ViewData(null, ac.create(rsrc, id));
@@ -342,11 +342,11 @@
if (path.isEmpty()) {
if (isRead(req)) {
viewData = new ViewData(null, c.list());
- } else if (c instanceof AcceptsPost && "POST".equals(req.getMethod())) {
+ } else if (c instanceof AcceptsPost && isPost(req)) {
@SuppressWarnings("unchecked")
AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) c;
viewData = new ViewData(null, ac.post(rsrc));
- } else if (c instanceof AcceptsDelete && "DELETE".equals(req.getMethod())) {
+ } else if (c instanceof AcceptsDelete && isDelete(req)) {
@SuppressWarnings("unchecked")
AcceptsDelete<RestResource> ac = (AcceptsDelete<RestResource>) c;
viewData = new ViewData(null, ac.delete(rsrc, null));
@@ -361,16 +361,12 @@
checkPreconditions(req);
viewData = new ViewData(null, null);
} catch (ResourceNotFoundException e) {
- if (c instanceof AcceptsCreate
- && path.isEmpty()
- && ("POST".equals(req.getMethod()) || "PUT".equals(req.getMethod()))) {
+ if (c instanceof AcceptsCreate && path.isEmpty() && (isPost(req) || isPut(req))) {
@SuppressWarnings("unchecked")
AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) c;
viewData = new ViewData(viewData.pluginName, ac.create(rsrc, id));
status = SC_CREATED;
- } else if (c instanceof AcceptsDelete
- && path.isEmpty()
- && "DELETE".equals(req.getMethod())) {
+ } else if (c instanceof AcceptsDelete && path.isEmpty() && isDelete(req)) {
@SuppressWarnings("unchecked")
AcceptsDelete<RestResource> ac = (AcceptsDelete<RestResource>) c;
viewData = new ViewData(viewData.pluginName, ac.delete(rsrc, id));
@@ -436,10 +432,7 @@
responseBytes = replyJson(req, res, qp.config(), result);
}
}
- } catch (MalformedJsonException e) {
- responseBytes =
- replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
- } catch (JsonParseException e) {
+ } catch (MalformedJsonException | JsonParseException e) {
responseBytes =
replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
} catch (BadRequestException e) {
@@ -519,16 +512,17 @@
private static HttpServletRequest applyXdOverrides(HttpServletRequest req, QueryParams qp)
throws BadRequestException {
- if (!"POST".equals(req.getMethod())) {
+ if (!isPost(req)) {
throw new BadRequestException("POST required");
}
String method = qp.xdMethod();
String contentType = qp.xdContentType();
if (method.equals("POST") || method.equals("PUT")) {
- if (!"text/plain".equals(req.getContentType())) {
+ if (!isType(PLAIN_TEXT, req.getContentType())) {
throw new BadRequestException("invalid " + CONTENT_TYPE);
- } else if (Strings.isNullOrEmpty(contentType)) {
+ }
+ if (Strings.isNullOrEmpty(contentType)) {
throw new BadRequestException(XD_CONTENT_TYPE + " required");
}
}
@@ -603,7 +597,7 @@
res.setStatus(SC_OK);
setCorsHeaders(res, origin);
- res.setContentType("text/plain");
+ res.setContentType(PLAIN_TEXT);
res.setContentLength(0);
}
@@ -751,13 +745,17 @@
br.skip(Long.MAX_VALUE);
}
}
- } else if (rawInputRequest(req, type)) {
+ }
+ if (rawInputRequest(req, type)) {
return parseRawInput(req, type);
- } else if ("DELETE".equals(req.getMethod()) && hasNoBody(req)) {
+ }
+ if (isDelete(req) && hasNoBody(req)) {
return null;
- } else if (hasNoBody(req)) {
+ }
+ if (hasNoBody(req)) {
return createInstance(type);
- } else if (isType("text/plain", req.getContentType())) {
+ }
+ if (isType(PLAIN_TEXT, req.getContentType())) {
try (BufferedReader br = req.getReader()) {
char[] tmp = new char[256];
StringBuilder sb = new StringBuilder();
@@ -767,11 +765,11 @@
}
return parseString(sb.toString(), type);
}
- } else if ("POST".equals(req.getMethod()) && isType(FORM_TYPE, req.getContentType())) {
- return OutputFormat.JSON.newGson().fromJson(ParameterParser.formToJson(req), type);
- } else {
- throw new BadRequestException("Expected Content-Type: " + JSON_TYPE);
}
+ if (isPost(req) && isType(FORM_TYPE, req.getContentType())) {
+ return OutputFormat.JSON.newGson().fromJson(ParameterParser.formToJson(req), type);
+ }
+ throw new BadRequestException("Expected Content-Type: " + JSON_TYPE);
}
private void consumeRawInputRequestBody(HttpServletRequest req, Type type) throws IOException {
@@ -922,9 +920,7 @@
FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES.translateName( //
field.getDeclaringClass().getDeclaredField(field.getName()));
names.put(field.getName(), name);
- } catch (SecurityException e) {
- return true;
- } catch (NoSuchFieldException e) {
+ } catch (SecurityException | NoSuchFieldException e) {
return true;
}
}
@@ -1024,7 +1020,7 @@
}
res.setHeader("X-FYI-Content-Encoding", "base64");
res.setHeader("X-FYI-Content-Type", src.getContentType());
- return b64.setContentType("text/plain").setCharacterEncoding(ISO_8859_1);
+ return b64.setContentType(PLAIN_TEXT).setCharacterEncoding(ISO_8859_1);
}
private static BinaryResult stackGzip(HttpServletResponse res, BinaryResult src)
@@ -1033,7 +1029,8 @@
long len = src.getContentLength();
if (len < 256) {
return src; // Do not compress very small payloads.
- } else if (len <= (10 << 20)) {
+ }
+ if (len <= (10 << 20)) {
gz = compress(src);
if (len <= gz.getContentLength()) {
return src;
@@ -1082,12 +1079,10 @@
return new ViewData(p.get(0), view);
}
view = views.get(p.get(0), "GET." + viewname);
- if (view != null) {
- if (view instanceof AcceptsPost && "POST".equals(method)) {
- @SuppressWarnings("unchecked")
- AcceptsPost<RestResource> ap = (AcceptsPost<RestResource>) view;
- return new ViewData(p.get(0), ap.post(rsrc));
- }
+ if (view != null && view instanceof AcceptsPost && "POST".equals(method)) {
+ @SuppressWarnings("unchecked")
+ AcceptsPost<RestResource> ap = (AcceptsPost<RestResource>) view;
+ return new ViewData(p.get(0), ap.post(rsrc));
}
throw new ResourceNotFoundException(projection);
}
@@ -1115,14 +1110,14 @@
if (r.size() == 1) {
Map.Entry<String, RestView<RestResource>> entry = Iterables.getOnlyElement(r.entrySet());
return new ViewData(entry.getKey(), entry.getValue());
- } else if (r.isEmpty()) {
- throw new ResourceNotFoundException(projection);
- } else {
- throw new AmbiguousViewException(
- String.format(
- "Projection %s is ambiguous: %s",
- name, r.keySet().stream().map(in -> in + "~" + projection).collect(joining(", "))));
}
+ if (r.isEmpty()) {
+ throw new ResourceNotFoundException(projection);
+ }
+ throw new AmbiguousViewException(
+ String.format(
+ "Projection %s is ambiguous: %s",
+ name, r.keySet().stream().map(in -> in + "~" + projection).collect(joining(", "))));
}
private static List<IdString> splitPath(HttpServletRequest req) {
@@ -1162,6 +1157,18 @@
}
}
+ private boolean isDelete(HttpServletRequest req) {
+ return "DELETE".equals(req.getMethod());
+ }
+
+ private static boolean isPost(HttpServletRequest req) {
+ return "POST".equals(req.getMethod());
+ }
+
+ private boolean isPut(HttpServletRequest req) {
+ return "PUT".equals(req.getMethod());
+ }
+
private static boolean isRead(HttpServletRequest req) {
return "GET".equals(req.getMethod()) || "HEAD".equals(req.getMethod());
}
@@ -1180,7 +1187,7 @@
if (!Strings.isNullOrEmpty(req.getQueryString())) {
uri += "?" + req.getQueryString();
}
- log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
+ log.error("Error in {} {}", req.getMethod(), uri, err);
if (!res.isCommitted()) {
res.reset();
@@ -1224,7 +1231,7 @@
if (!text.endsWith("\n")) {
text += "\n";
}
- return replyBinaryResult(req, res, BinaryResult.create(text).setContentType("text/plain"));
+ return replyBinaryResult(req, res, BinaryResult.create(text).setContentType(PLAIN_TEXT));
}
private static boolean isMaybeHTML(String text) {
@@ -1246,12 +1253,14 @@
private static boolean isType(String expect, String given) {
if (given == null) {
return false;
- } else if (expect.equals(given)) {
- return true;
- } else if (given.startsWith(expect + ",")) {
+ }
+ if (expect.equals(given)) {
return true;
}
- for (String p : given.split("[ ,;][ ,;]*")) {
+ if (given.startsWith(expect + ",")) {
+ return true;
+ }
+ for (String p : Splitter.on(TYPE_SPLIT_PATTERN).split(given)) {
if (expect.equals(p)) {
return true;
}
diff --git a/java/com/google/gerrit/launcher/GerritLauncher.java b/java/com/google/gerrit/launcher/GerritLauncher.java
index 618d754..e8892be 100644
--- a/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -704,9 +704,7 @@
private static boolean includeJar(URL u) {
String path = u.getPath();
- return path.endsWith(".jar")
- && !path.endsWith("-src.jar")
- && !path.contains("/buck-out/gen/lib/gwt/");
+ return path.endsWith(".jar") && !path.endsWith("-src.jar");
}
private GerritLauncher() {}
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index b30e66c..468aa67 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -586,12 +586,14 @@
private void decodeReviewers(ListMultimap<String, IndexableField> doc, ChangeData cd) {
cd.setReviewers(
ChangeField.parseReviewerFieldValues(
+ cd.getId(),
FluentIterable.from(doc.get(REVIEWER_FIELD)).transform(IndexableField::stringValue)));
}
private void decodeReviewersByEmail(ListMultimap<String, IndexableField> doc, ChangeData cd) {
cd.setReviewersByEmail(
ChangeField.parseReviewerByEmailFieldValues(
+ cd.getId(),
FluentIterable.from(doc.get(REVIEWER_BY_EMAIL_FIELD))
.transform(IndexableField::stringValue)));
}
@@ -599,6 +601,7 @@
private void decodePendingReviewers(ListMultimap<String, IndexableField> doc, ChangeData cd) {
cd.setPendingReviewers(
ChangeField.parseReviewerFieldValues(
+ cd.getId(),
FluentIterable.from(doc.get(PENDING_REVIEWER_FIELD))
.transform(IndexableField::stringValue)));
}
@@ -607,6 +610,7 @@
ListMultimap<String, IndexableField> doc, ChangeData cd) {
cd.setPendingReviewersByEmail(
ChangeField.parseReviewerByEmailFieldValues(
+ cd.getId(),
FluentIterable.from(doc.get(PENDING_REVIEWER_BY_EMAIL_FIELD))
.transform(IndexableField::stringValue)));
}
diff --git a/java/com/google/gerrit/lucene/LuceneIndexModule.java b/java/com/google/gerrit/lucene/LuceneIndexModule.java
index d5d6360..e44c9c6 100644
--- a/java/com/google/gerrit/lucene/LuceneIndexModule.java
+++ b/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -18,6 +18,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.Schema;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.index.IndexModule;
@@ -37,21 +38,21 @@
import org.eclipse.jgit.lib.Config;
public class LuceneIndexModule extends AbstractModule {
- public static LuceneIndexModule singleVersionAllLatest(int threads) {
- return new LuceneIndexModule(ImmutableMap.<String, Integer>of(), threads, false);
+ public static LuceneIndexModule singleVersionAllLatest(int threads, boolean slave) {
+ return new LuceneIndexModule(ImmutableMap.of(), threads, false, slave);
}
public static LuceneIndexModule singleVersionWithExplicitVersions(
- Map<String, Integer> versions, int threads) {
- return new LuceneIndexModule(versions, threads, false);
+ Map<String, Integer> versions, int threads, boolean slave) {
+ return new LuceneIndexModule(versions, threads, false, slave);
}
- public static LuceneIndexModule latestVersionWithOnlineUpgrade() {
- return new LuceneIndexModule(null, 0, true);
+ public static LuceneIndexModule latestVersionWithOnlineUpgrade(boolean slave) {
+ return new LuceneIndexModule(null, 0, true, slave);
}
- public static LuceneIndexModule latestVersionWithoutOnlineUpgrade() {
- return new LuceneIndexModule(null, 0, false);
+ public static LuceneIndexModule latestVersionWithoutOnlineUpgrade(boolean slave) {
+ return new LuceneIndexModule(null, 0, false, slave);
}
static boolean isInMemoryTest(Config cfg) {
@@ -61,36 +62,45 @@
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) {
+ 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() {
- install(
- new FactoryModuleBuilder()
- .implement(AccountIndex.class, LuceneAccountIndex.class)
- .build(AccountIndex.Factory.class));
- install(
- new FactoryModuleBuilder()
- .implement(ChangeIndex.class, LuceneChangeIndex.class)
- .build(ChangeIndex.Factory.class));
+ 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 FactoryModuleBuilder()
- .implement(ProjectIndex.class, LuceneProjectIndex.class)
- .build(ProjectIndex.Factory.class));
- install(new IndexModule(threads));
+
+ install(new IndexModule(threads, slave));
if (singleVersions == null) {
install(new MultiVersionModule());
} else {
@@ -98,6 +108,11 @@
}
}
+ @SuppressWarnings("unused")
+ private static <T> T createDummyIndexFactory(Schema<?> schema) {
+ throw new UnsupportedOperationException();
+ }
+
@Provides
@Singleton
IndexConfig getIndexConfig(@GerritServerConfig Config cfg) {
diff --git a/java/com/google/gerrit/lucene/LuceneVersionManager.java b/java/com/google/gerrit/lucene/LuceneVersionManager.java
index c7c802f..ce92432 100644
--- a/java/com/google/gerrit/lucene/LuceneVersionManager.java
+++ b/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -78,7 +78,7 @@
Path p = getDir(sitePaths, def.getName(), schema);
boolean isDir = Files.isDirectory(p);
if (Files.exists(p) && !isDir) {
- log.warn("Not a directory: %s", p.toAbsolutePath());
+ log.warn("Not a directory: {}", p.toAbsolutePath());
}
int v = schema.getVersion();
versions.put(v, new Version<>(schema, v, isDir, cfg.getReady(def.getName(), v)));
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index 3e7ea1b..d73a4c7 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -71,7 +71,7 @@
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.DummyIndexModule;
+import com.google.gerrit.server.group.PeriodicGroupIndexer;
import com.google.gerrit.server.index.IndexModule;
import com.google.gerrit.server.index.IndexModule.IndexType;
import com.google.gerrit.server.index.VersionManager;
@@ -334,9 +334,7 @@
}
cfgInjector = createCfgInjector();
config = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
- if (!slave) {
- initIndexType();
- }
+ initIndexType();
sysInjector = createSysInjector();
sysInjector.getInstance(PluginGuiceEnvironment.class).setDbCfgInjector(dbInjector, cfgInjector);
manager.add(dbInjector, cfgInjector, sysInjector);
@@ -472,7 +470,9 @@
}
});
modules.add(new GarbageCollectionModule());
- if (!slave) {
+ if (slave) {
+ modules.add(new PeriodicGroupIndexer.Module());
+ } else {
modules.add(new AccountDeactivator.Module());
modules.add(new ChangeCleanupRunner.Module());
}
@@ -493,9 +493,6 @@
}
private Module createIndexModule() {
- if (slave) {
- return new DummyIndexModule();
- }
if (luceneModule != null) {
return luceneModule;
}
@@ -506,12 +503,12 @@
switch (indexType) {
case LUCENE:
return onlineUpgrade
- ? LuceneIndexModule.latestVersionWithOnlineUpgrade()
- : LuceneIndexModule.latestVersionWithoutOnlineUpgrade();
+ ? LuceneIndexModule.latestVersionWithOnlineUpgrade(slave)
+ : LuceneIndexModule.latestVersionWithoutOnlineUpgrade(slave);
case ELASTICSEARCH:
return onlineUpgrade
- ? ElasticIndexModule.latestVersionWithOnlineUpgrade()
- : ElasticIndexModule.latestVersionWithoutOnlineUpgrade();
+ ? ElasticIndexModule.latestVersionWithOnlineUpgrade(slave)
+ : ElasticIndexModule.latestVersionWithoutOnlineUpgrade(slave);
default:
throw new IllegalStateException("unsupported index.type = " + indexType);
}
diff --git a/java/com/google/gerrit/pgm/MigrateToNoteDb.java b/java/com/google/gerrit/pgm/MigrateToNoteDb.java
index 8cd148f..2340a97 100644
--- a/java/com/google/gerrit/pgm/MigrateToNoteDb.java
+++ b/java/com/google/gerrit/pgm/MigrateToNoteDb.java
@@ -33,6 +33,7 @@
import com.google.gerrit.server.index.DummyIndexModule;
import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator;
+import com.google.gerrit.server.schema.DataSourceType;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
@@ -46,8 +47,10 @@
"Trial mode: migrate changes and turn on reading from NoteDb, but leave ReviewDb as the"
+ " source of truth";
+ private static final int ISSUE_8022_THREAD_LIMIT = 4;
+
@Option(name = "--threads", usage = "Number of threads to use for rebuilding NoteDb")
- private int threads = Runtime.getRuntime().availableProcessors();
+ private Integer threads;
@Option(
name = "--project",
@@ -105,12 +108,13 @@
try {
mustHaveValidSite();
dbInjector = createDbInjector(MULTI_USER);
- threads = ThreadLimiter.limitThreads(dbInjector, threads);
dbManager = new LifecycleManager();
dbManager.add(dbInjector);
dbManager.start();
+ threads = limitThreads();
+
sysInjector = createSysInjector();
sysInjector.injectMembers(this);
sysManager = new LifecycleManager();
@@ -158,6 +162,27 @@
return reindexPgm.main(reindexArgs.stream().toArray(String[]::new));
}
+ private int limitThreads() {
+ if (threads != null) {
+ return threads;
+ }
+ int actualThreads;
+ int procs = Runtime.getRuntime().availableProcessors();
+ DataSourceType dsType = dbInjector.getInstance(DataSourceType.class);
+ if (dsType.getDriver().equals("org.h2.Driver") && procs > ISSUE_8022_THREAD_LIMIT) {
+ System.out.println(
+ "Not using more than "
+ + ISSUE_8022_THREAD_LIMIT
+ + " threads due to http://crbug.com/gerrit/8022");
+ System.out.println("Can be increased by passing --threads, but may cause errors");
+ actualThreads = ISSUE_8022_THREAD_LIMIT;
+ } else {
+ actualThreads = procs;
+ }
+ actualThreads = ThreadLimiter.limitThreads(dbInjector, actualThreads);
+ return actualThreads;
+ }
+
private Injector createSysInjector() {
return dbInjector.createChildInjector(
new FactoryModule() {
diff --git a/java/com/google/gerrit/pgm/Passwd.java b/java/com/google/gerrit/pgm/Passwd.java
index d9c3c5d..e4b362c 100644
--- a/java/com/google/gerrit/pgm/Passwd.java
+++ b/java/com/google/gerrit/pgm/Passwd.java
@@ -14,6 +14,7 @@
package com.google.gerrit.pgm;
+import com.google.common.base.Splitter;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.pgm.init.api.ConsoleUI;
import com.google.gerrit.pgm.init.api.InstallAllPlugins;
@@ -49,13 +50,13 @@
private String password;
private void init() {
- String[] varParts = sectionAndKey.split("\\.");
- if (varParts.length != 2) {
+ List<String> varParts = Splitter.on('.').splitToList(sectionAndKey);
+ if (varParts.size() != 2) {
throw new IllegalArgumentException(
"Invalid name '" + sectionAndKey + "': expected section.key format");
}
- section = varParts[0];
- key = varParts[1];
+ section = varParts.get(0);
+ key = varParts.get(1);
}
@Override
diff --git a/java/com/google/gerrit/pgm/ProtoGen.java b/java/com/google/gerrit/pgm/ProtoGen.java
index f659bb0..61a0bd5 100644
--- a/java/com/google/gerrit/pgm/ProtoGen.java
+++ b/java/com/google/gerrit/pgm/ProtoGen.java
@@ -58,8 +58,7 @@
header = new String(buf.array(), ptr, len, UTF_8);
}
- String version = com.google.gerrit.common.Version.getVersion();
- out.write(header.replace("@@VERSION@@", version));
+ out.write(header);
jsm.generateProto(out);
out.flush();
}
@@ -69,7 +68,6 @@
} finally {
lock.unlock();
}
- System.out.println("Created " + file.getPath());
return 0;
}
}
diff --git a/java/com/google/gerrit/pgm/Reindex.java b/java/com/google/gerrit/pgm/Reindex.java
index d8451d5..15a330e 100644
--- a/java/com/google/gerrit/pgm/Reindex.java
+++ b/java/com/google/gerrit/pgm/Reindex.java
@@ -84,7 +84,6 @@
dbInjector = createDbInjector(MULTI_USER);
globalConfig = dbInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
threads = ThreadLimiter.limitThreads(dbInjector, threads);
- checkNotSlaveMode();
overrideConfig();
LifecycleManager dbManager = new LifecycleManager();
dbManager.add(dbInjector);
@@ -141,25 +140,21 @@
"invalid index name(s): " + new TreeSet<>(invalid) + " available indices are: " + valid);
}
- private void checkNotSlaveMode() throws Die {
- if (globalConfig.getBoolean("container", "slave", false)) {
- throw die("Cannot run reindex in slave mode");
- }
- }
-
private Injector createSysInjector() {
Map<String, Integer> versions = new HashMap<>();
if (changesVersion != null) {
versions.put(ChangeSchemaDefinitions.INSTANCE.getName(), changesVersion);
}
+ boolean slave = globalConfig.getBoolean("container", "slave", false);
List<Module> modules = new ArrayList<>();
Module indexModule;
switch (IndexModule.getIndexType(dbInjector)) {
case LUCENE:
- indexModule = LuceneIndexModule.singleVersionWithExplicitVersions(versions, threads);
+ indexModule = LuceneIndexModule.singleVersionWithExplicitVersions(versions, threads, slave);
break;
case ELASTICSEARCH:
- indexModule = ElasticIndexModule.singleVersionWithExplicitVersions(versions, threads);
+ indexModule =
+ ElasticIndexModule.singleVersionWithExplicitVersions(versions, threads, slave);
break;
default:
throw new IllegalStateException("unsupported index.type");
diff --git a/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index be61061..bc562cc 100644
--- a/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -121,6 +121,8 @@
extractMailExample("Footer.soy");
extractMailExample("FooterHtml.soy");
extractMailExample("HeaderHtml.soy");
+ extractMailExample("InboundEmailRejection.soy");
+ extractMailExample("InboundEmailRejectionHtml.soy");
extractMailExample("Merged.soy");
extractMailExample("MergedHtml.soy");
extractMailExample("NewChange.soy");
diff --git a/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java b/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
index f994432..95ff8d7 100644
--- a/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
+++ b/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
@@ -18,6 +18,7 @@
import static com.google.gerrit.pgm.init.api.InitUtil.savePublic;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.base.Splitter;
import com.google.gerrit.pgm.init.api.ConsoleUI;
import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.pgm.init.api.InitStep;
@@ -168,7 +169,7 @@
if (url.contains("?")) {
final int q = url.indexOf('?');
- for (String pair : url.substring(q + 1).split("&")) {
+ for (String pair : Splitter.on('&').split(url.substring(q + 1))) {
final int eq = pair.indexOf('=');
if (0 < eq) {
return false;
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index 760afdc..7af6c21 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -54,7 +54,6 @@
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.VisibleRefFilter;
import com.google.gerrit.server.git.receive.ReceiveCommitsExecutorModule;
import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
import com.google.gerrit.server.notedb.NoteDbModule;
@@ -134,7 +133,6 @@
factory(MergeUtil.Factory.class);
factory(PatchSetInserter.Factory.class);
factory(RebaseChangeOp.Factory.class);
- factory(VisibleRefFilter.Factory.class);
// As Reindex is a batch program, don't assume the index is available for
// the change cache.
diff --git a/java/com/google/gerrit/server/CurrentUser.java b/java/com/google/gerrit/server/CurrentUser.java
index 37e5a34..ace06c5 100644
--- a/java/com/google/gerrit/server/CurrentUser.java
+++ b/java/com/google/gerrit/server/CurrentUser.java
@@ -95,6 +95,11 @@
return Optional.empty();
}
+ /** @return unique name of the user for logging, never {@code null} */
+ public String getLoggableName() {
+ return getUserName().orElseGet(() -> getClass().getSimpleName());
+ }
+
/** Check if user is the IdentifiedUser */
public boolean isIdentifiedUser() {
return false;
@@ -125,11 +130,10 @@
* Lookup a previously stored property.
*
* @param key unique property key.
- * @return previously stored value, or {@code null}.
+ * @return previously stored value, or {@code Optional#empty()}.
*/
- @Nullable
- public <T> T get(PropertyKey<T> key) {
- return null;
+ public <T> Optional<T> get(PropertyKey<T> key) {
+ return Optional.empty();
}
/**
@@ -144,7 +148,7 @@
put(lastLoginExternalIdPropertyKey, externalIdKey);
}
- public ExternalId.Key getLastLoginExternalIdKey() {
+ public Optional<ExternalId.Key> getLastLoginExternalIdKey() {
return get(lastLoginExternalIdPropertyKey);
}
}
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index 72469f2..8379a7c 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -323,6 +323,7 @@
}
/** @return unique name of the user for logging, never {@code null} */
+ @Override
public String getLoggableName() {
return getUserName()
.orElseGet(
@@ -450,14 +451,13 @@
}
@Override
- @Nullable
- public synchronized <T> T get(PropertyKey<T> key) {
+ public synchronized <T> Optional<T> get(PropertyKey<T> key) {
if (properties != null) {
@SuppressWarnings("unchecked")
T value = (T) properties.get(key);
- return value;
+ return Optional.ofNullable(value);
}
- return null;
+ return Optional.empty();
}
/**
diff --git a/java/com/google/gerrit/server/account/AccountConfig.java b/java/com/google/gerrit/server/account/AccountConfig.java
index 2631ea9..8e1acbc 100644
--- a/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/java/com/google/gerrit/server/account/AccountConfig.java
@@ -74,9 +74,6 @@
*
* <p>The commit date of the first commit on the user branch is used as registration date of the
* account. The first commit may be an empty commit (since all config files are optional).
- *
- * <p>By default preferences and project watches are lazily parsed on need. Eager parsing can be
- * requested by {@link #setEagerParsing(boolean)}.
*/
public class AccountConfig extends VersionedMetaData implements ValidationError.Sink {
private final Account.Id accountId;
@@ -88,7 +85,6 @@
private ProjectWatches projectWatches;
private Preferences preferences;
private Optional<InternalAccountUpdate> accountUpdate = Optional.empty();
- private boolean eagerParsing;
private List<ValidationError> validationErrors;
public AccountConfig(Account.Id accountId, Repository allUsersRepo) {
@@ -97,21 +93,6 @@
this.ref = RefNames.refsUsers(accountId);
}
- /**
- * Sets whether all account data should be eagerly parsed.
- *
- * <p>Eager parsing should only be used if the caller is interested in validation errors for all
- * account data (see {@link #getValidationErrors()}.
- *
- * @param eagerParsing whether all account data should be eagerly parsed
- * @return this AccountConfig instance for chaining
- */
- public AccountConfig setEagerParsing(boolean eagerParsing) {
- checkState(loadedAccountProperties == null, "Account %s already loaded", accountId.get());
- this.eagerParsing = eagerParsing;
- return this;
- }
-
@Override
protected String getRefName() {
return ref;
@@ -264,10 +245,8 @@
Preferences.readDefaultConfig(repo),
this);
- if (eagerParsing) {
- projectWatches.parse();
- preferences.parse();
- }
+ projectWatches.parse();
+ preferences.parse();
} else {
loadedAccountProperties = Optional.empty();
@@ -365,9 +344,6 @@
/**
* Get the validation errors, if any were discovered during parsing the account data.
*
- * <p>To get validation errors for all account data request eager parsing before loading the
- * account (see {@link #setEagerParsing(boolean)}).
- *
* @return list of errors; empty list if there are no errors.
*/
public List<ValidationError> getValidationErrors() {
diff --git a/java/com/google/gerrit/server/account/AccountDeactivator.java b/java/com/google/gerrit/server/account/AccountDeactivator.java
index 0b05b36..73c47ad 100644
--- a/java/com/google/gerrit/server/account/AccountDeactivator.java
+++ b/java/com/google/gerrit/server/account/AccountDeactivator.java
@@ -14,19 +14,18 @@
package com.google.gerrit.server.account;
-import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG;
-
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.ScheduleConfig;
+import com.google.gerrit.server.config.ScheduleConfig.Schedule;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.query.account.AccountPredicates;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.inject.Inject;
import com.google.inject.Provider;
-import java.util.concurrent.TimeUnit;
+import java.util.Optional;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,13 +45,13 @@
private final WorkQueue queue;
private final AccountDeactivator deactivator;
private final boolean supportAutomaticAccountActivityUpdate;
- private final ScheduleConfig scheduleConfig;
+ private final Optional<Schedule> schedule;
@Inject
Lifecycle(WorkQueue queue, AccountDeactivator deactivator, @GerritServerConfig Config cfg) {
this.queue = queue;
this.deactivator = deactivator;
- scheduleConfig = new ScheduleConfig(cfg, "accountDeactivation");
+ schedule = ScheduleConfig.createSchedule(cfg, "accountDeactivation");
supportAutomaticAccountActivityUpdate =
cfg.getBoolean("auth", "autoUpdateAccountActiveStatus", false);
}
@@ -62,19 +61,7 @@
if (!supportAutomaticAccountActivityUpdate) {
return;
}
- long interval = scheduleConfig.getInterval();
- long delay = scheduleConfig.getInitialDelay();
- if (delay == MISSING_CONFIG && interval == MISSING_CONFIG) {
- log.info("Ignoring missing accountDeactivator schedule configuration");
- } else if (delay < 0 || interval <= 0) {
- log.warn(
- String.format(
- "Ignoring invalid accountDeactivator schedule configuration: %s", scheduleConfig));
- } else {
- queue
- .getDefaultQueue()
- .scheduleAtFixedRate(deactivator, delay, interval, TimeUnit.MILLISECONDS);
- }
+ schedule.ifPresent(s -> queue.scheduleAtFixedRate(deactivator, s));
}
@Override
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index ee049c0..017bcaa 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -116,7 +116,7 @@
}
}
- if (nameOrEmail.matches(ExternalId.USER_NAME_PATTERN_REGEX)) {
+ if (ExternalId.isValidUsername(nameOrEmail)) {
Optional<AccountState> who = byId.getByUsername(nameOrEmail);
if (who.isPresent()) {
return ImmutableSet.of(who.map(a -> a.getAccount().getId()).get());
diff --git a/java/com/google/gerrit/server/account/AccountSshKey.java b/java/com/google/gerrit/server/account/AccountSshKey.java
index 4457fec..aeccc0a 100644
--- a/java/com/google/gerrit/server/account/AccountSshKey.java
+++ b/java/com/google/gerrit/server/account/AccountSshKey.java
@@ -14,8 +14,10 @@
package com.google.gerrit.server.account;
+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}. */
@@ -83,9 +85,9 @@
private String getPublicKeyPart(int index, String defaultValue) {
String s = getSshPublicKey();
if (s != null && s.length() > 0) {
- String[] parts = s.split(" ");
- if (parts.length > index) {
- return parts[index];
+ List<String> parts = Splitter.on(' ').splitToList(s);
+ if (parts.size() > index) {
+ return parts.get(index);
}
}
return defaultValue;
diff --git a/java/com/google/gerrit/server/account/AccountState.java b/java/com/google/gerrit/server/account/AccountState.java
index 34f4eb1..5b9ea69 100644
--- a/java/com/google/gerrit/server/account/AccountState.java
+++ b/java/com/google/gerrit/server/account/AccountState.java
@@ -19,7 +19,6 @@
import com.google.common.base.Function;
import com.google.common.base.Strings;
-import com.google.common.base.Suppliers;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableMap;
@@ -40,7 +39,6 @@
import java.io.IOException;
import java.util.Collection;
import java.util.Optional;
-import java.util.function.Supplier;
import org.apache.commons.codec.DecoderException;
import org.eclipse.jgit.lib.ObjectId;
import org.slf4j.Logger;
@@ -108,18 +106,27 @@
: accountConfig.getExternalIdsRev();
ImmutableSet<ExternalId> extIds =
extIdsRev.isPresent()
- ? externalIds.byAccount(account.getId(), extIdsRev.get())
+ ? ImmutableSet.copyOf(externalIds.byAccount(account.getId(), extIdsRev.get()))
: ImmutableSet.of();
+ // 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();
+ DiffPreferencesInfo diffPreferences = accountConfig.getDiffPreferences();
+ EditPreferencesInfo editPreferences = accountConfig.getEditPreferences();
+
return Optional.of(
new AccountState(
allUsersName,
account,
extIds,
- Suppliers.memoize(() -> accountConfig.getProjectWatches()),
- Suppliers.memoize(() -> accountConfig.getGeneralPreferences()),
- Suppliers.memoize(() -> accountConfig.getDiffPreferences()),
- Suppliers.memoize(() -> accountConfig.getEditPreferences())));
+ projectWatches,
+ generalPreferences,
+ diffPreferences,
+ editPreferences));
}
/**
@@ -148,30 +155,30 @@
allUsersName,
account,
ImmutableSet.copyOf(extIds),
- Suppliers.ofInstance(ImmutableMap.of()),
- Suppliers.ofInstance(GeneralPreferencesInfo.defaults()),
- Suppliers.ofInstance(DiffPreferencesInfo.defaults()),
- Suppliers.ofInstance(EditPreferencesInfo.defaults()));
+ ImmutableMap.of(),
+ GeneralPreferencesInfo.defaults(),
+ DiffPreferencesInfo.defaults(),
+ EditPreferencesInfo.defaults());
}
private final AllUsersName allUsersName;
private final Account account;
private final ImmutableSet<ExternalId> externalIds;
private final Optional<String> userName;
- private final Supplier<ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>>> projectWatches;
- private final Supplier<GeneralPreferencesInfo> generalPreferences;
- private final Supplier<DiffPreferencesInfo> diffPreferences;
- private final Supplier<EditPreferencesInfo> editPreferences;
+ private final ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches;
+ private final GeneralPreferencesInfo generalPreferences;
+ private final DiffPreferencesInfo diffPreferences;
+ private final EditPreferencesInfo editPreferences;
private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
private AccountState(
AllUsersName allUsersName,
Account account,
ImmutableSet<ExternalId> externalIds,
- Supplier<ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>>> projectWatches,
- Supplier<GeneralPreferencesInfo> generalPreferences,
- Supplier<DiffPreferencesInfo> diffPreferences,
- Supplier<EditPreferencesInfo> editPreferences) {
+ ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches,
+ GeneralPreferencesInfo generalPreferences,
+ DiffPreferencesInfo diffPreferences,
+ EditPreferencesInfo editPreferences) {
this.allUsersName = allUsersName;
this.account = account;
this.externalIds = externalIds;
@@ -239,22 +246,22 @@
/** The project watches of the account. */
public ImmutableMap<ProjectWatchKey, ImmutableSet<NotifyType>> getProjectWatches() {
- return projectWatches.get();
+ return projectWatches;
}
/** The general preferences of the account. */
public GeneralPreferencesInfo getGeneralPreferences() {
- return generalPreferences.get();
+ return generalPreferences;
}
/** The diff preferences of the account. */
public DiffPreferencesInfo getDiffPreferences() {
- return diffPreferences.get();
+ return diffPreferences;
}
/** The edit preferences of the account. */
public EditPreferencesInfo getEditPreferences() {
- return editPreferences.get();
+ return editPreferences;
}
/**
diff --git a/java/com/google/gerrit/server/account/AuthorizedKeys.java b/java/com/google/gerrit/server/account/AuthorizedKeys.java
index 6f30888..3a6c032 100644
--- a/java/com/google/gerrit/server/account/AuthorizedKeys.java
+++ b/java/com/google/gerrit/server/account/AuthorizedKeys.java
@@ -15,11 +15,13 @@
package com.google.gerrit.server.account;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
import com.google.gerrit.reviewdb.client.Account;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
+import java.util.regex.Pattern;
public class AuthorizedKeys {
public static final String FILE_NAME = "authorized_keys";
@@ -28,10 +30,12 @@
@VisibleForTesting public static final String DELETED_KEY_COMMENT = "# DELETED";
+ private static final Pattern LINE_SPLIT_PATTERN = Pattern.compile("\\r?\\n");
+
public static List<Optional<AccountSshKey>> parse(Account.Id accountId, String s) {
List<Optional<AccountSshKey>> keys = new ArrayList<>();
int seq = 1;
- for (String line : s.split("\\r?\\n")) {
+ for (String line : Splitter.on(LINE_SPLIT_PATTERN).split(s)) {
line = line.trim();
if (line.isEmpty()) {
continue;
diff --git a/java/com/google/gerrit/server/account/CreateGroupArgs.java b/java/com/google/gerrit/server/account/CreateGroupArgs.java
index 0c0778c..5bcb84b 100644
--- a/java/com/google/gerrit/server/account/CreateGroupArgs.java
+++ b/java/com/google/gerrit/server/account/CreateGroupArgs.java
@@ -22,7 +22,7 @@
private AccountGroup.NameKey groupName;
public String groupDescription;
public boolean visibleToAll;
- public AccountGroup.Id ownerGroupId;
+ public AccountGroup.UUID ownerGroupUuid;
public Collection<? extends Account.Id> initialMembers;
public AccountGroup.NameKey getGroup() {
diff --git a/java/com/google/gerrit/server/account/GroupCache.java b/java/com/google/gerrit/server/account/GroupCache.java
index 545998a..8133d9c 100644
--- a/java/com/google/gerrit/server/account/GroupCache.java
+++ b/java/com/google/gerrit/server/account/GroupCache.java
@@ -16,7 +16,6 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.group.InternalGroup;
-import java.io.IOException;
import java.util.Optional;
/** Tracks group objects in memory for efficient access. */
@@ -48,11 +47,45 @@
*/
Optional<InternalGroup> get(AccountGroup.UUID groupUuid);
- /** Notify the cache that a new group was constructed. */
- void onCreateGroup(AccountGroup.UUID groupUuid) throws IOException;
+ /**
+ * Removes the association of the given ID with a group.
+ *
+ * <p>The next call to {@link #get(AccountGroup.Id)} won't provide a cached value.
+ *
+ * <p>It's safe to call this method if no association exists.
+ *
+ * <p><strong>Note: </strong>This method doesn't touch any associations between names/UUIDs and
+ * groups!
+ *
+ * @param groupId the ID of a possibly associated group
+ */
+ void evict(AccountGroup.Id groupId);
- void evict(AccountGroup.UUID groupUuid, AccountGroup.Id groupId, AccountGroup.NameKey groupName)
- throws IOException;
+ /**
+ * Removes the association of the given name with a group.
+ *
+ * <p>The next call to {@link #get(AccountGroup.NameKey)} won't provide a cached value.
+ *
+ * <p>It's safe to call this method if no association exists.
+ *
+ * <p><strong>Note: </strong>This method doesn't touch any associations between IDs/UUIDs and
+ * groups!
+ *
+ * @param groupName the name of a possibly associated group
+ */
+ void evict(AccountGroup.NameKey groupName);
- void evictAfterRename(AccountGroup.NameKey oldName) throws IOException;
+ /**
+ * Removes the association of the given UUID with a group.
+ *
+ * <p>The next call to {@link #get(AccountGroup.UUID)} won't provide a cached value.
+ *
+ * <p>It's safe to call this method if no association exists.
+ *
+ * <p><strong>Note: </strong>This method doesn't touch any associations between names/IDs and
+ * groups!
+ *
+ * @param groupUuid the UUID of a possibly associated group
+ */
+ void evict(AccountGroup.UUID groupUuid);
}
diff --git a/java/com/google/gerrit/server/account/GroupCacheImpl.java b/java/com/google/gerrit/server/account/GroupCacheImpl.java
index 4783f29..58eaadc 100644
--- a/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -21,8 +21,6 @@
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.db.Groups;
-import com.google.gerrit.server.index.group.GroupIndexCollection;
-import com.google.gerrit.server.index.group.GroupIndexer;
import com.google.gerrit.server.query.group.InternalGroupQuery;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
@@ -31,10 +29,8 @@
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
-import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
-import java.util.function.BooleanSupplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -72,18 +68,15 @@
private final LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId;
private final LoadingCache<String, Optional<InternalGroup>> byName;
private final LoadingCache<String, Optional<InternalGroup>> byUUID;
- private final Provider<GroupIndexer> indexer;
@Inject
GroupCacheImpl(
@Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<InternalGroup>> byId,
@Named(BYNAME_NAME) LoadingCache<String, Optional<InternalGroup>> byName,
- @Named(BYUUID_NAME) LoadingCache<String, Optional<InternalGroup>> byUUID,
- Provider<GroupIndexer> indexer) {
+ @Named(BYUUID_NAME) LoadingCache<String, Optional<InternalGroup>> byUUID) {
this.byId = byId;
this.byName = byName;
this.byUUID = byUUID;
- this.indexer = indexer;
}
@Override
@@ -97,29 +90,6 @@
}
@Override
- public void evict(
- AccountGroup.UUID groupUuid, AccountGroup.Id groupId, AccountGroup.NameKey groupName)
- throws IOException {
- if (groupId != null) {
- byId.invalidate(groupId);
- }
- if (groupName != null) {
- byName.invalidate(groupName.get());
- }
- if (groupUuid != null) {
- byUUID.invalidate(groupUuid.get());
- }
- indexer.get().index(groupUuid);
- }
-
- @Override
- public void evictAfterRename(AccountGroup.NameKey oldName) throws IOException {
- if (oldName != null) {
- byName.invalidate(oldName.get());
- }
- }
-
- @Override
public Optional<InternalGroup> get(AccountGroup.NameKey name) {
if (name == null) {
return Optional.empty();
@@ -147,34 +117,37 @@
}
@Override
- public void onCreateGroup(AccountGroup.UUID groupUuid) throws IOException {
- indexer.get().index(groupUuid);
+ public void evict(AccountGroup.Id groupId) {
+ if (groupId != null) {
+ byId.invalidate(groupId);
+ }
+ }
+
+ @Override
+ public void evict(AccountGroup.NameKey groupName) {
+ if (groupName != null) {
+ byName.invalidate(groupName.get());
+ }
+ }
+
+ @Override
+ public void evict(AccountGroup.UUID groupUuid) {
+ if (groupUuid != null) {
+ byUUID.invalidate(groupUuid.get());
+ }
}
static class ByIdLoader extends CacheLoader<AccountGroup.Id, Optional<InternalGroup>> {
- private final SchemaFactory<ReviewDb> schema;
- private final BooleanSupplier hasGroupIndex;
private final Provider<InternalGroupQuery> groupQueryProvider;
@Inject
- ByIdLoader(
- SchemaFactory<ReviewDb> schema,
- GroupIndexCollection groupIndexCollection,
- Provider<InternalGroupQuery> groupQueryProvider) {
- this.schema = schema;
- hasGroupIndex = () -> groupIndexCollection.getSearchIndex() != null;
+ ByIdLoader(Provider<InternalGroupQuery> groupQueryProvider) {
this.groupQueryProvider = groupQueryProvider;
}
@Override
public Optional<InternalGroup> load(AccountGroup.Id key) throws Exception {
- if (hasGroupIndex.getAsBoolean()) {
- return groupQueryProvider.get().byId(key);
- }
-
- try (ReviewDb db = schema.open()) {
- return Groups.getGroupFromReviewDb(db, key);
- }
+ return groupQueryProvider.get().byId(key);
}
}
diff --git a/java/com/google/gerrit/server/account/GroupControl.java b/java/com/google/gerrit/server/account/GroupControl.java
index 119dc5b..4b223bf 100644
--- a/java/com/google/gerrit/server/account/GroupControl.java
+++ b/java/com/google/gerrit/server/account/GroupControl.java
@@ -20,15 +20,12 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.group.InternalGroup;
-import com.google.gerrit.server.group.InternalGroupDescription;
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 com.google.inject.Provider;
import com.google.inject.Singleton;
-import java.util.Optional;
/** Access control management for a group of accounts managed in Gerrit. */
public class GroupControl {
@@ -56,30 +53,16 @@
public static class Factory {
private final PermissionBackend permissionBackend;
- private final GroupCache groupCache;
private final Provider<CurrentUser> user;
private final GroupBackend groupBackend;
@Inject
- Factory(
- PermissionBackend permissionBackend,
- GroupCache gc,
- Provider<CurrentUser> cu,
- GroupBackend gb) {
+ Factory(PermissionBackend permissionBackend, Provider<CurrentUser> cu, GroupBackend gb) {
this.permissionBackend = permissionBackend;
- groupCache = gc;
user = cu;
groupBackend = gb;
}
- public GroupControl controlFor(AccountGroup.Id groupId) throws NoSuchGroupException {
- Optional<InternalGroup> group = groupCache.get(groupId);
- return group
- .map(InternalGroupDescription::new)
- .map(this::controlFor)
- .orElseThrow(() -> new NoSuchGroupException(groupId));
- }
-
public GroupControl controlFor(AccountGroup.UUID groupId) throws NoSuchGroupException {
final GroupDescription.Basic group = groupBackend.get(groupId);
if (group == null) {
diff --git a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 66b6b10..d8472a6 100644
--- a/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -21,16 +21,12 @@
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Streams;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.db.Groups;
-import com.google.gerrit.server.index.group.GroupField;
-import com.google.gerrit.server.index.group.GroupIndex;
-import com.google.gerrit.server.index.group.GroupIndexCollection;
import com.google.gerrit.server.query.group.InternalGroupQuery;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
@@ -145,81 +141,41 @@
static class GroupsWithMemberLoader
extends CacheLoader<Account.Id, ImmutableSet<AccountGroup.UUID>> {
- private final SchemaFactory<ReviewDb> schema;
- private final Provider<GroupIndex> groupIndexProvider;
private final Provider<InternalGroupQuery> groupQueryProvider;
- private final GroupCache groupCache;
@Inject
- GroupsWithMemberLoader(
- SchemaFactory<ReviewDb> schema,
- GroupIndexCollection groupIndexCollection,
- Provider<InternalGroupQuery> groupQueryProvider,
- GroupCache groupCache) {
- this.schema = schema;
- groupIndexProvider = groupIndexCollection::getSearchIndex;
+ GroupsWithMemberLoader(Provider<InternalGroupQuery> groupQueryProvider) {
this.groupQueryProvider = groupQueryProvider;
- this.groupCache = groupCache;
}
@Override
public ImmutableSet<AccountGroup.UUID> load(Account.Id memberId) throws OrmException {
- GroupIndex groupIndex = groupIndexProvider.get();
- if (groupIndex != null && groupIndex.getSchema().hasField(GroupField.MEMBER)) {
- return groupQueryProvider
- .get()
- .byMember(memberId)
- .stream()
- .map(InternalGroup::getGroupUUID)
- .collect(toImmutableSet());
- }
- try (ReviewDb db = schema.open()) {
- return Groups.getGroupsWithMemberFromReviewDb(db, memberId)
- .map(groupCache::get)
- .flatMap(Streams::stream)
- .map(InternalGroup::getGroupUUID)
- .collect(toImmutableSet());
- }
+ return groupQueryProvider
+ .get()
+ .byMember(memberId)
+ .stream()
+ .map(InternalGroup::getGroupUUID)
+ .collect(toImmutableSet());
}
}
static class ParentGroupsLoader
extends CacheLoader<AccountGroup.UUID, ImmutableList<AccountGroup.UUID>> {
- private final SchemaFactory<ReviewDb> schema;
- private final Provider<GroupIndex> groupIndexProvider;
private final Provider<InternalGroupQuery> groupQueryProvider;
- private final GroupCache groupCache;
@Inject
- ParentGroupsLoader(
- SchemaFactory<ReviewDb> sf,
- GroupIndexCollection groupIndexCollection,
- Provider<InternalGroupQuery> groupQueryProvider,
- GroupCache groupCache) {
- schema = sf;
- this.groupIndexProvider = groupIndexCollection::getSearchIndex;
+ ParentGroupsLoader(Provider<InternalGroupQuery> groupQueryProvider) {
this.groupQueryProvider = groupQueryProvider;
- this.groupCache = groupCache;
}
@Override
public ImmutableList<AccountGroup.UUID> load(AccountGroup.UUID key) throws OrmException {
- GroupIndex groupIndex = groupIndexProvider.get();
- if (groupIndex != null && groupIndex.getSchema().hasField(GroupField.SUBGROUP)) {
- return groupQueryProvider
- .get()
- .bySubgroup(key)
- .stream()
- .map(InternalGroup::getGroupUUID)
- .collect(toImmutableList());
- }
- try (ReviewDb db = schema.open()) {
- return Groups.getParentGroupsFromReviewDb(db, key)
- .map(groupCache::get)
- .flatMap(Streams::stream)
- .map(InternalGroup::getGroupUUID)
- .collect(toImmutableList());
- }
+ return groupQueryProvider
+ .get()
+ .bySubgroup(key)
+ .stream()
+ .map(InternalGroup::getGroupUUID)
+ .collect(toImmutableList());
}
}
diff --git a/java/com/google/gerrit/server/account/HashedPassword.java b/java/com/google/gerrit/server/account/HashedPassword.java
index 0323f4e..427c8c3 100644
--- a/java/com/google/gerrit/server/account/HashedPassword.java
+++ b/java/com/google/gerrit/server/account/HashedPassword.java
@@ -15,10 +15,12 @@
package com.google.gerrit.server.account;
import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Ints;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
+import java.util.List;
import org.apache.commons.codec.DecoderException;
import org.bouncycastle.crypto.generators.BCrypt;
import org.bouncycastle.util.Arrays;
@@ -46,12 +48,12 @@
throw new DecoderException("unrecognized algorithm");
}
- String[] fields = encoded.split(":");
- if (fields.length != 4) {
+ List<String> fields = Splitter.on(':').splitToList(encoded);
+ if (fields.size() != 4) {
throw new DecoderException("want 4 fields");
}
- Integer cost = Ints.tryParse(fields[1]);
+ Integer cost = Ints.tryParse(fields.get(1));
if (cost == null) {
throw new DecoderException("cost parse failed");
}
@@ -60,11 +62,11 @@
throw new DecoderException("cost should be 4..31 inclusive, got " + cost);
}
- byte[] salt = codec.decode(fields[2]);
+ byte[] salt = codec.decode(fields.get(2));
if (salt.length != 16) {
throw new DecoderException("salt should be 16 bytes, got " + salt.length);
}
- return new HashedPassword(codec.decode(fields[3]), salt, cost);
+ return new HashedPassword(codec.decode(fields.get(3)), salt, cost);
}
private static byte[] hashPassword(String password, byte[] salt, int cost) {
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 3b6ebd1..ffd413a 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -51,7 +51,7 @@
private static final String USER_NAME_PATTERN_LAST_REGEX = "[a-zA-Z0-9]";
/** Regular expression that a username must match. */
- public static final String USER_NAME_PATTERN_REGEX =
+ private static final String USER_NAME_PATTERN_REGEX =
"^"
+ //
"("
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
index ffb4e76..a8844cd 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
@@ -14,11 +14,11 @@
package com.google.gerrit.server.account.externalids;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.SetMultimap;
import com.google.gerrit.reviewdb.client.Account;
import java.io.IOException;
import java.util.Collection;
+import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
/**
@@ -26,6 +26,8 @@
*
* <p>On each cache access the SHA1 of the refs/meta/external-ids branch is read to verify that the
* cache is up to date.
+ *
+ * <p>All returned collections are unmodifiable.
*/
interface ExternalIdCache {
void onReplace(
@@ -35,17 +37,17 @@
Collection<ExternalId> toAdd)
throws IOException;
- ImmutableSet<ExternalId> byAccount(Account.Id accountId) throws IOException;
+ Set<ExternalId> byAccount(Account.Id accountId) throws IOException;
- ImmutableSet<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException;
+ Set<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException;
- ImmutableSetMultimap<Account.Id, ExternalId> allByAccount() throws IOException;
+ SetMultimap<Account.Id, ExternalId> allByAccount() throws IOException;
- ImmutableSetMultimap<String, ExternalId> byEmails(String... emails) throws IOException;
+ SetMultimap<String, ExternalId> byEmails(String... emails) throws IOException;
- ImmutableSetMultimap<String, ExternalId> allByEmail() throws IOException;
+ SetMultimap<String, ExternalId> allByEmail() throws IOException;
- default ImmutableSet<ExternalId> byEmail(String email) throws IOException {
+ default Set<ExternalId> byEmail(String email) throws IOException {
return byEmails(email).get(email);
}
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
index 25789a1..1f77773 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -14,23 +14,23 @@
package com.google.gerrit.server.account.externalids;
-import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
-
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
-import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.SetMultimap;
import com.google.gerrit.reviewdb.client.Account;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
+import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -95,22 +95,22 @@
}
@Override
- public ImmutableSet<ExternalId> byAccount(Account.Id accountId) throws IOException {
+ public Set<ExternalId> byAccount(Account.Id accountId) throws IOException {
return get().byAccount().get(accountId);
}
@Override
- public ImmutableSet<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException {
+ public Set<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException {
return get(rev).byAccount().get(accountId);
}
@Override
- public ImmutableSetMultimap<Account.Id, ExternalId> allByAccount() throws IOException {
+ public SetMultimap<Account.Id, ExternalId> allByAccount() throws IOException {
return get().byAccount();
}
@Override
- public ImmutableSetMultimap<String, ExternalId> byEmails(String... emails) throws IOException {
+ public SetMultimap<String, ExternalId> byEmails(String... emails) throws IOException {
AllExternalIds allExternalIds = get();
ImmutableSetMultimap.Builder<String, ExternalId> byEmails = ImmutableSetMultimap.builder();
for (String email : emails) {
@@ -120,7 +120,7 @@
}
@Override
- public ImmutableSetMultimap<String, ExternalId> allByEmail() throws IOException {
+ public SetMultimap<String, ExternalId> allByEmail() throws IOException {
return get().byEmail();
}
@@ -179,21 +179,30 @@
}
}
+ /**
+ * Cache value containing all external IDs.
+ *
+ * <p>All returned fields are unmodifiable.
+ */
@AutoValue
abstract static class AllExternalIds {
static AllExternalIds create(Multimap<Account.Id, ExternalId> byAccount) {
- ImmutableSetMultimap<String, ExternalId> byEmail =
- byAccount
- .values()
- .stream()
- .filter(e -> !Strings.isNullOrEmpty(e.email()))
- .collect(toImmutableSetMultimap(ExternalId::email, e -> e));
+ SetMultimap<String, ExternalId> byEmailCopy =
+ MultimapBuilder.hashKeys(byAccount.size()).hashSetValues(1).build();
+ byAccount
+ .values()
+ .stream()
+ .filter(e -> !Strings.isNullOrEmpty(e.email()))
+ .forEach(e -> byEmailCopy.put(e.email(), e));
+
return new AutoValue_ExternalIdCacheImpl_AllExternalIds(
- ImmutableSetMultimap.copyOf(byAccount), byEmail);
+ Multimaps.unmodifiableSetMultimap(
+ MultimapBuilder.hashKeys(byAccount.size()).hashSetValues(5).build(byAccount)),
+ byEmailCopy);
}
- public abstract ImmutableSetMultimap<Account.Id, ExternalId> byAccount();
+ public abstract SetMultimap<Account.Id, ExternalId> byAccount();
- public abstract ImmutableSetMultimap<String, ExternalId> byEmail();
+ public abstract SetMultimap<String, ExternalId> byEmail();
}
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIds.java b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
index 11f5855c..167af45 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
@@ -17,12 +17,13 @@
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSetMultimap;
+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.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
@@ -66,13 +67,12 @@
}
/** Returns the external IDs of the specified account. */
- public ImmutableSet<ExternalId> byAccount(Account.Id accountId) throws IOException {
+ public Set<ExternalId> byAccount(Account.Id accountId) throws IOException {
return externalIdCache.byAccount(accountId);
}
/** Returns the external IDs of the specified account that have the given scheme. */
- public ImmutableSet<ExternalId> byAccount(Account.Id accountId, String scheme)
- throws IOException {
+ public Set<ExternalId> byAccount(Account.Id accountId, String scheme) throws IOException {
return byAccount(accountId)
.stream()
.filter(e -> e.key().isScheme(scheme))
@@ -80,12 +80,12 @@
}
/** Returns the external IDs of the specified account. */
- public ImmutableSet<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException {
+ public Set<ExternalId> byAccount(Account.Id accountId, ObjectId rev) throws IOException {
return externalIdCache.byAccount(accountId, rev);
}
/** Returns the external IDs of the specified account that have the given scheme. */
- public ImmutableSet<ExternalId> byAccount(Account.Id accountId, String scheme, ObjectId rev)
+ public Set<ExternalId> byAccount(Account.Id accountId, String scheme, ObjectId rev)
throws IOException {
return byAccount(accountId, rev)
.stream()
@@ -94,7 +94,7 @@
}
/** Returns all external IDs by account. */
- public ImmutableSetMultimap<Account.Id, ExternalId> allByAccount() throws IOException {
+ public SetMultimap<Account.Id, ExternalId> allByAccount() throws IOException {
return externalIdCache.allByAccount();
}
@@ -111,7 +111,7 @@
*
* @see #byEmails(String...)
*/
- public ImmutableSet<ExternalId> byEmail(String email) throws IOException {
+ public Set<ExternalId> byEmail(String email) throws IOException {
return externalIdCache.byEmail(email);
}
@@ -129,12 +129,12 @@
*
* @see #byEmail(String)
*/
- public ImmutableSetMultimap<String, ExternalId> byEmails(String... emails) throws IOException {
+ public SetMultimap<String, ExternalId> byEmails(String... emails) throws IOException {
return externalIdCache.byEmails(emails);
}
/** Returns all external IDs by email. */
- public ImmutableSetMultimap<String, ExternalId> allByEmail() throws IOException {
+ public SetMultimap<String, ExternalId> allByEmail() throws IOException {
return externalIdCache.allByEmail();
}
}
diff --git a/java/com/google/gerrit/server/api/projects/TagApiImpl.java b/java/com/google/gerrit/server/api/projects/TagApiImpl.java
index 84af4e8..03e2162 100644
--- a/java/com/google/gerrit/server/api/projects/TagApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/TagApiImpl.java
@@ -22,6 +22,7 @@
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.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.TagResource;
import com.google.gerrit.server.restapi.project.CreateTag;
@@ -88,7 +89,7 @@
}
}
- private TagResource resource() throws RestApiException, IOException {
+ private TagResource resource() throws RestApiException, IOException, PermissionBackendException {
return tags.parse(project, IdString.fromDecoded(ref));
}
}
diff --git a/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index 01ebb1f..c7d3f73 100644
--- a/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -91,7 +91,7 @@
}
private Account.Id createAccountByLdap(String user) throws CmdLineException, IOException {
- if (!user.matches(ExternalId.USER_NAME_PATTERN_REGEX)) {
+ if (!ExternalId.isValidUsername(user)) {
throw new CmdLineException(owner, "user \"" + user + "\" not found");
}
diff --git a/java/com/google/gerrit/server/args4j/ChangeIdHandler.java b/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
index 0e841ec..adb5f63 100644
--- a/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.args4j;
+import com.google.common.base.Splitter;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
@@ -23,6 +24,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
+import java.util.List;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.OptionDef;
@@ -47,16 +49,16 @@
@Override
public final int parseArguments(Parameters params) throws CmdLineException {
final String token = params.getParameter(0);
- final String[] tokens = token.split(",");
- if (tokens.length != 3) {
+ final List<String> tokens = Splitter.on(',').splitToList(token);
+ if (tokens.size() != 3) {
throw new CmdLineException(
owner, "change should be specified as <project>,<branch>,<change-id>");
}
try {
- final Change.Key key = Change.Key.parse(tokens[2]);
- final Project.NameKey project = new Project.NameKey(tokens[0]);
- final Branch.NameKey branch = new Branch.NameKey(project, tokens[1]);
+ final Change.Key key = Change.Key.parse(tokens.get(2));
+ final Project.NameKey project = new Project.NameKey(tokens.get(0));
+ final Branch.NameKey branch = new Branch.NameKey(project, tokens.get(1));
for (ChangeData cd : queryProvider.get().byBranchKey(branch, key)) {
setter.addValue(cd.getId());
return 1;
diff --git a/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java b/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java
index 5a3d82c..ae26e12 100644
--- a/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java
+++ b/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java
@@ -23,14 +23,14 @@
/**
* Gets the acting user who is updating the group.
*
- * @return the {@link Account.Id} of the acting user.
+ * @return the {@link com.google.gerrit.reviewdb.client.Account.Id} of the acting user.
*/
Account.Id getActor();
/**
- * Gets the {@link AccountGroup.UUID} of the updated group.
+ * Gets the {@link com.google.gerrit.reviewdb.client.AccountGroup.UUID} of the updated group.
*
- * @return the {@link AccountGroup.UUID} of the updated group.
+ * @return the {@link com.google.gerrit.reviewdb.client.AccountGroup.UUID} of the updated group.
*/
AccountGroup.UUID getUpdatedGroup();
diff --git a/java/com/google/gerrit/server/change/ChangeCleanupRunner.java b/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
index eb7d099..a887760 100644
--- a/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
+++ b/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
@@ -14,13 +14,10 @@
package com.google.gerrit.server.change;
-import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG;
-
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.ChangeCleanupConfig;
-import com.google.gerrit.server.config.ScheduleConfig;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.UpdateException;
@@ -28,8 +25,6 @@
import com.google.gerrit.server.util.OneOffRequestContext;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -58,22 +53,7 @@
@Override
public void start() {
- ScheduleConfig scheduleConfig = cfg.getScheduleConfig();
- long interval = scheduleConfig.getInterval();
- long delay = scheduleConfig.getInitialDelay();
- if (delay == MISSING_CONFIG && interval == MISSING_CONFIG) {
- log.info("Ignoring missing changeCleanup schedule configuration");
- } else if (delay < 0 || interval <= 0) {
- log.warn(
- String.format(
- "Ignoring invalid changeCleanup schedule configuration: %s", scheduleConfig));
- } else {
- @SuppressWarnings("unused")
- Future<?> possiblyIgnoredError =
- queue
- .getDefaultQueue()
- .scheduleAtFixedRate(runner, delay, interval, TimeUnit.MILLISECONDS);
- }
+ cfg.getSchedule().ifPresent(s -> queue.scheduleAtFixedRate(runner, s));
}
@Override
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index c058e33..113a6bf 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -656,11 +656,11 @@
return result;
}
- private boolean submittable(ChangeData cd) throws OrmException {
+ private boolean submittable(ChangeData cd) {
return SubmitRecord.findOkRecord(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)).isPresent();
}
- private List<SubmitRecord> submitRecords(ChangeData cd) throws OrmException {
+ private List<SubmitRecord> submitRecords(ChangeData cd) {
return cd.submitRecords(SUBMIT_RULE_OPTIONS_LENIENT);
}
@@ -712,7 +712,7 @@
}
private Map<String, LabelWithStatus> initLabels(
- ChangeData cd, LabelTypes labelTypes, boolean standard) throws OrmException {
+ ChangeData cd, LabelTypes labelTypes, boolean standard) {
Map<String, LabelWithStatus> labels = new TreeMap<>(labelTypes.nameComparator());
for (SubmitRecord rec : submitRecords(cd)) {
if (rec.labels == null) {
diff --git a/java/com/google/gerrit/server/change/ReviewerSuggestion.java b/java/com/google/gerrit/server/change/ReviewerSuggestion.java
index a2dd8b5..198a5fd 100644
--- a/java/com/google/gerrit/server/change/ReviewerSuggestion.java
+++ b/java/com/google/gerrit/server/change/ReviewerSuggestion.java
@@ -24,19 +24,20 @@
/**
* Listener to provide reviewer suggestions.
*
- * <p>Invoked by Gerrit a user who is searching for a reviewer to add to a change.
+ * <p>Invoked by Gerrit when a user clicks "Add Reviewer" on a change.
*/
@ExtensionPoint
public interface ReviewerSuggestion {
/**
- * Reviewer suggestion.
+ * Suggest reviewers to add to a change.
*
* @param project The name key of the project the suggestion is for.
- * @param changeId The changeId that the suggestion is for. Can be an {@code null}.
- * @param query The query as typed by the user. Can be an {@code null}.
+ * @param changeId The changeId that the suggestion is for. Can be {@code null}.
+ * @param query The query as typed by the user. Can be {@code null}.
* @param candidates A set of candidates for the ranking. Can be empty.
- * @return Set of suggested reviewers as a tuple of account id and score. The account ids listed
- * here don't have to be a part of candidates.
+ * @return Set of {@link SuggestedReviewer}s. The {@link
+ * com.google.gerrit.reviewdb.client.Account.Id}s listed here don't have to be included in
+ * {@code candidates}.
*/
Set<SuggestedReviewer> suggestReviewers(
Project.NameKey project,
diff --git a/java/com/google/gerrit/server/config/ChangeCleanupConfig.java b/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
index b2b5fab..632293e 100644
--- a/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
+++ b/java/com/google/gerrit/server/config/ChangeCleanupConfig.java
@@ -16,8 +16,10 @@
import com.google.common.base.Strings;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.config.ScheduleConfig.Schedule;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Config;
@@ -33,7 +35,7 @@
+ "\n"
+ "If this change is still wanted it should be restored.";
- private final ScheduleConfig scheduleConfig;
+ private final Optional<Schedule> schedule;
private final long abandonAfter;
private final boolean abandonIfMergeable;
private final String abandonMessage;
@@ -41,7 +43,7 @@
@Inject
ChangeCleanupConfig(
@GerritServerConfig Config cfg, @CanonicalWebUrl @Nullable String canonicalWebUrl) {
- scheduleConfig = new ScheduleConfig(cfg, SECTION);
+ schedule = ScheduleConfig.createSchedule(cfg, SECTION);
abandonAfter = readAbandonAfter(cfg);
abandonIfMergeable = cfg.getBoolean(SECTION, null, KEY_ABANDON_IF_MERGEABLE, true);
abandonMessage = readAbandonMessage(cfg, canonicalWebUrl);
@@ -64,8 +66,8 @@
return abandonMessage;
}
- public ScheduleConfig getScheduleConfig() {
- return scheduleConfig;
+ public Optional<Schedule> getSchedule() {
+ return schedule;
}
public long getAbandonAfter() {
diff --git a/java/com/google/gerrit/server/config/GcConfig.java b/java/com/google/gerrit/server/config/GcConfig.java
index 36f5e29..3e28827 100644
--- a/java/com/google/gerrit/server/config/GcConfig.java
+++ b/java/com/google/gerrit/server/config/GcConfig.java
@@ -14,24 +14,26 @@
package com.google.gerrit.server.config;
+import com.google.gerrit.server.config.ScheduleConfig.Schedule;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.util.Optional;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ConfigConstants;
@Singleton
public class GcConfig {
- private final ScheduleConfig scheduleConfig;
+ private final Optional<Schedule> schedule;
private final boolean aggressive;
@Inject
GcConfig(@GerritServerConfig Config cfg) {
- scheduleConfig = new ScheduleConfig(cfg, ConfigConstants.CONFIG_GC_SECTION);
+ schedule = ScheduleConfig.createSchedule(cfg, ConfigConstants.CONFIG_GC_SECTION);
aggressive = cfg.getBoolean(ConfigConstants.CONFIG_GC_SECTION, "aggressive", false);
}
- public ScheduleConfig getScheduleConfig() {
- return scheduleConfig;
+ public Optional<Schedule> getSchedule() {
+ return schedule;
}
public boolean isAggressive() {
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index e29968b..77312be 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -121,7 +121,6 @@
import com.google.gerrit.server.git.ReceivePackInitializer;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.TransferConfig;
-import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.git.receive.ReceiveCommitsModule;
import com.google.gerrit.server.git.strategy.SubmitStrategy;
import com.google.gerrit.server.git.validators.CommitValidationListener;
@@ -137,6 +136,7 @@
import com.google.gerrit.server.git.validators.UploadValidationListener;
import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.server.index.change.ReindexAfterRefUpdate;
+import com.google.gerrit.server.mail.AutoReplyMailFilter;
import com.google.gerrit.server.mail.EmailModule;
import com.google.gerrit.server.mail.ListMailFilter;
import com.google.gerrit.server.mail.MailFilter;
@@ -146,6 +146,7 @@
import com.google.gerrit.server.mail.send.DeleteReviewerSender;
import com.google.gerrit.server.mail.send.FromAddressGenerator;
import com.google.gerrit.server.mail.send.FromAddressGeneratorProvider;
+import com.google.gerrit.server.mail.send.InboundEmailRejectionSender;
import com.google.gerrit.server.mail.send.MailSoyTofuProvider;
import com.google.gerrit.server.mail.send.MailTemplates;
import com.google.gerrit.server.mail.send.MergedSender;
@@ -266,7 +267,7 @@
factory(RegisterNewEmailSender.Factory.class);
factory(ReplacePatchSetSender.Factory.class);
factory(SetAssigneeSender.Factory.class);
- factory(VisibleRefFilter.Factory.class);
+ factory(InboundEmailRejectionSender.Factory.class);
bind(PermissionCollection.Factory.class);
bind(AccountVisibility.class).toProvider(AccountVisibilityProvider.class).in(SINGLETON);
factory(ProjectOwnerGroupsProvider.Factory.class);
@@ -393,6 +394,9 @@
DynamicMap.mapOf(binder(), MailFilter.class);
bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
+ bind(AutoReplyMailFilter.class)
+ .annotatedWith(Exports.named("AutoReplyMailFilter"))
+ .to(AutoReplyMailFilter.class);
factory(UploadValidators.Factory.class);
DynamicSet.setOf(binder(), UploadValidationListener.class);
diff --git a/java/com/google/gerrit/server/config/ScheduleConfig.java b/java/com/google/gerrit/server/config/ScheduleConfig.java
index f7f8238..9fff101 100644
--- a/java/com/google/gerrit/server/config/ScheduleConfig.java
+++ b/java/com/google/gerrit/server/config/ScheduleConfig.java
@@ -14,10 +14,13 @@
package com.google.gerrit.server.config;
+import static com.google.common.base.Preconditions.checkNotNull;
import static java.time.ZoneId.systemDefault;
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
import com.google.common.annotations.VisibleForTesting;
-import java.text.MessageFormat;
+import com.google.gerrit.common.Nullable;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalTime;
@@ -26,158 +29,181 @@
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.util.Locale;
+import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public class ScheduleConfig {
+/**
+ * This class reads a schedule for running a periodic background job from a Git config.
+ *
+ * <p>A schedule configuration consists of two parameters:
+ *
+ * <ul>
+ * <li>{@code interval}: Interval for running the periodic background job. The interval must be
+ * larger than zero. The following suffixes are supported to define the time unit for the
+ * interval:
+ * <ul>
+ * <li>{@code s}, {@code sec}, {@code second}, {@code seconds}
+ * <li>{@code m}, {@code min}, {@code minute}, {@code minutes}
+ * <li>{@code h}, {@code hr}, {@code hour}, {@code hours}
+ * <li>{@code d}, {@code day}, {@code days}
+ * <li>{@code w}, {@code week}, {@code weeks} ({@code 1 week} is treated as {@code 7 days})
+ * <li>{@code mon}, {@code month}, {@code months} ({@code 1 month} is treated as {@code 30
+ * days})
+ * <li>{@code y}, {@code year}, {@code years} ({@code 1 year} is treated as {@code 365
+ * days})
+ * </ul>
+ * <li>{@code startTime}: The start time defines the first execution of the periodic background
+ * job. If the configured {@code interval} is shorter than {@code startTime - now} the start
+ * time will be preponed by the maximum integral multiple of {@code interval} so that the
+ * start time is still in the future. {@code startTime} must have one of the following
+ * formats:
+ * <ul>
+ * <li>{@code <day of week> <hours>:<minutes>}
+ * <li>{@code <hours>:<minutes>}
+ * </ul>
+ * The placeholders can have the following values:
+ * <ul>
+ * <li>{@code <day of week>}: {@code Mon}, {@code Tue}, {@code Wed}, {@code Thu}, {@code
+ * Fri}, {@code Sat}, {@code Sun}
+ * <li>{@code <hours>}: {@code 00}-{@code 23}
+ * <li>{@code <minutes>}: {@code 00}-{@code 59}
+ * </ul>
+ * The timezone cannot be specified but is always the system default time-zone.
+ * </ul>
+ *
+ * <p>The section and the subsection from which the {@code interval} and {@code startTime}
+ * parameters are read can be configured.
+ *
+ * <p>Examples for a schedule configuration:
+ *
+ * <ul>
+ * <li>
+ * <pre>
+ * foo.startTime = Fri 10:30
+ * foo.interval = 2 day
+ * </pre>
+ * Assuming that the server is started on {@code Mon 7:00} then {@code startTime - now} is
+ * {@code 4 days 3:30 hours}. This is larger than the interval hence the start time is
+ * preponed by the maximum integral multiple of the interval so that start time is still in
+ * the future, i.e. preponed by 4 days. This yields a start time of {@code Mon 10:30}, next
+ * executions are {@code Wed 10:30}, {@code Fri 10:30}. etc.
+ * <li>
+ * <pre>
+ * foo.startTime = 6:00
+ * foo.interval = 1 day
+ * </pre>
+ * Assuming that the server is started on {@code Mon 7:00} then this yields the first run on
+ * next Tuesday at 6:00 and a repetition interval of 1 day.
+ * </ul>
+ */
+@AutoValue
+public abstract class ScheduleConfig {
private static final Logger log = LoggerFactory.getLogger(ScheduleConfig.class);
- public static final long MISSING_CONFIG = -1L;
- public static final long INVALID_CONFIG = -2L;
- private static final String KEY_INTERVAL = "interval";
- private static final String KEY_STARTTIME = "startTime";
- private final Config rc;
- private final String section;
- private final String subsection;
- private final String keyInterval;
- private final String keyStartTime;
- private final long initialDelay;
- private final long interval;
+ @VisibleForTesting static final String KEY_INTERVAL = "interval";
+ @VisibleForTesting static final String KEY_STARTTIME = "startTime";
- public ScheduleConfig(Config rc, String section) {
- this(rc, section, null);
+ private static final long MISSING_CONFIG = -1L;
+ private static final long INVALID_CONFIG = -2L;
+
+ public static Optional<Schedule> createSchedule(Config config, String section) {
+ return builder(config, section).buildSchedule();
}
- public ScheduleConfig(Config rc, String section, String subsection) {
- this(rc, section, subsection, ZonedDateTime.now(systemDefault()));
+ public static ScheduleConfig.Builder builder(Config config, String section) {
+ return new AutoValue_ScheduleConfig.Builder()
+ .setNow(computeNow())
+ .setKeyInterval(KEY_INTERVAL)
+ .setKeyStartTime(KEY_STARTTIME)
+ .setConfig(config)
+ .setSection(section);
}
- public ScheduleConfig(
- Config rc, String section, String subsection, String keyInterval, String keyStartTime) {
- this(rc, section, subsection, keyInterval, keyStartTime, ZonedDateTime.now(systemDefault()));
- }
+ abstract Config config();
- @VisibleForTesting
- ScheduleConfig(Config rc, String section, String subsection, ZonedDateTime now) {
- this(rc, section, subsection, KEY_INTERVAL, KEY_STARTTIME, now);
- }
+ abstract String section();
- @VisibleForTesting
- ScheduleConfig(
- Config rc,
- String section,
- String subsection,
- String keyInterval,
- String keyStartTime,
- ZonedDateTime now) {
- this.rc = rc;
- this.section = section;
- this.subsection = subsection;
- this.keyInterval = keyInterval;
- this.keyStartTime = keyStartTime;
- this.interval = interval(rc, section, subsection, keyInterval);
+ @Nullable
+ abstract String subsection();
+
+ abstract String keyInterval();
+
+ abstract String keyStartTime();
+
+ abstract ZonedDateTime now();
+
+ @Memoized
+ public Optional<Schedule> schedule() {
+ long interval = computeInterval(config(), section(), subsection(), keyInterval());
+
+ long initialDelay;
if (interval > 0) {
- this.initialDelay = initialDelay(rc, section, subsection, keyStartTime, now, interval);
+ initialDelay =
+ computeInitialDelay(config(), section(), subsection(), keyStartTime(), now(), interval);
} else {
- this.initialDelay = interval;
+ initialDelay = interval;
}
+
+ if (isInvalidOrMissing(interval, initialDelay)) {
+ return Optional.empty();
+ }
+
+ return Optional.of(Schedule.create(interval, initialDelay));
}
- /**
- * Milliseconds between constructor invocation and first event time.
- *
- * <p>If there is any lag between the constructor invocation and queuing the object into an
- * executor the event will run later, as there is no method to adjust for the scheduling delay.
- */
- public long getInitialDelay() {
- return initialDelay;
- }
+ private boolean isInvalidOrMissing(long interval, long initialDelay) {
+ String key = section() + (subsection() != null ? "." + subsection() : "");
+ if (interval == MISSING_CONFIG && initialDelay == MISSING_CONFIG) {
+ log.info("No schedule configuration for \"{}\".", key);
+ return true;
+ }
- /** Number of milliseconds between events. */
- public long getInterval() {
- return interval;
- }
-
- private static long interval(Config rc, String section, String subsection, String keyInterval) {
- long interval = MISSING_CONFIG;
- try {
- interval =
- ConfigUtil.getTimeUnit(rc, section, subsection, keyInterval, -1, TimeUnit.MILLISECONDS);
- if (interval == MISSING_CONFIG) {
- log.info(
- MessageFormat.format(
- "{0} schedule parameter \"{0}.{1}\" is not configured", section, keyInterval));
- }
- } catch (IllegalArgumentException e) {
+ if (interval == MISSING_CONFIG) {
log.error(
- MessageFormat.format("Invalid {0} schedule parameter \"{0}.{1}\"", section, keyInterval),
- e);
- interval = INVALID_CONFIG;
+ "Incomplete schedule configuration for \"{}\" is ignored. Missing value for \"{}\".",
+ key,
+ key + "." + keyInterval());
+ return true;
}
- return interval;
- }
- private static long initialDelay(
- Config rc,
- String section,
- String subsection,
- String keyStartTime,
- ZonedDateTime now,
- long interval) {
- long delay = MISSING_CONFIG;
- String start = rc.getString(section, subsection, keyStartTime);
- try {
- if (start != null) {
- DateTimeFormatter formatter =
- DateTimeFormatter.ofPattern("[E ]HH:mm").withLocale(Locale.US);
- LocalTime firstStartTime = LocalTime.parse(start, formatter);
- ZonedDateTime startTime = now.with(firstStartTime);
- try {
- DayOfWeek dayOfWeek = formatter.parse(start, DayOfWeek::from);
- startTime = startTime.with(dayOfWeek);
- } catch (DateTimeParseException ignored) {
- // Day of week is an optional parameter.
- }
- startTime = startTime.truncatedTo(ChronoUnit.MINUTES);
- delay = Duration.between(now, startTime).toMillis() % interval;
- if (delay <= 0) {
- delay += interval;
- }
- } else {
- log.info(
- MessageFormat.format(
- "{0} schedule parameter \"{0}.{1}\" is not configured", section, keyStartTime));
- }
- } catch (IllegalArgumentException e2) {
+ if (initialDelay == MISSING_CONFIG) {
log.error(
- MessageFormat.format("Invalid {0} schedule parameter \"{0}.{1}\"", section, keyStartTime),
- e2);
- delay = INVALID_CONFIG;
+ "Incomplete schedule configuration for \"{}\" is ignored. Missing value for \"{}\".",
+ key,
+ key + "." + keyStartTime());
+ return true;
}
- return delay;
+
+ if (interval <= 0 || initialDelay < 0) {
+ log.error("Invalid schedule configuration for \"{}\" is ignored. ", key);
+ return true;
+ }
+
+ return false;
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
- b.append(formatValue(keyInterval));
+ b.append(formatValue(keyInterval()));
b.append(", ");
- b.append(formatValue(keyStartTime));
+ b.append(formatValue(keyStartTime()));
return b.toString();
}
private String formatValue(String key) {
StringBuilder b = new StringBuilder();
- b.append(section);
- if (subsection != null) {
+ b.append(section());
+ if (subsection() != null) {
b.append(".");
- b.append(subsection);
+ b.append(subsection());
}
b.append(".");
b.append(key);
- String value = rc.getString(section, subsection, key);
+ String value = config().getString(section(), subsection(), key);
if (value != null) {
b.append(" = ");
b.append(value);
@@ -186,4 +212,135 @@
}
return b.toString();
}
+
+ private static long computeInterval(
+ Config rc, String section, String subsection, String keyInterval) {
+ try {
+ return ConfigUtil.getTimeUnit(
+ rc, section, subsection, keyInterval, MISSING_CONFIG, TimeUnit.MILLISECONDS);
+ } catch (IllegalArgumentException e) {
+ return INVALID_CONFIG;
+ }
+ }
+
+ private static long computeInitialDelay(
+ Config rc,
+ String section,
+ String subsection,
+ String keyStartTime,
+ ZonedDateTime now,
+ long interval) {
+ String start = rc.getString(section, subsection, keyStartTime);
+ if (start == null) {
+ return MISSING_CONFIG;
+ }
+ return computeInitialDelay(interval, start, now);
+ }
+
+ private static long computeInitialDelay(long interval, String start) {
+ return computeInitialDelay(interval, start, computeNow());
+ }
+
+ private static long computeInitialDelay(long interval, String start, ZonedDateTime now) {
+ checkNotNull(start);
+
+ try {
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("[E ]HH:mm").withLocale(Locale.US);
+ LocalTime firstStartTime = LocalTime.parse(start, formatter);
+ ZonedDateTime startTime = now.with(firstStartTime);
+ try {
+ DayOfWeek dayOfWeek = formatter.parse(start, DayOfWeek::from);
+ startTime = startTime.with(dayOfWeek);
+ } catch (DateTimeParseException ignored) {
+ // Day of week is an optional parameter.
+ }
+ startTime = startTime.truncatedTo(ChronoUnit.MINUTES);
+ long delay = Duration.between(now, startTime).toMillis() % interval;
+ if (delay <= 0) {
+ delay += interval;
+ }
+ return delay;
+ } catch (DateTimeParseException e) {
+ return INVALID_CONFIG;
+ }
+ }
+
+ private static ZonedDateTime computeNow() {
+ return ZonedDateTime.now(systemDefault());
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setConfig(Config config);
+
+ public abstract Builder setSection(String section);
+
+ public abstract Builder setSubsection(@Nullable String subsection);
+
+ public abstract Builder setKeyInterval(String keyInterval);
+
+ public abstract Builder setKeyStartTime(String keyStartTime);
+
+ @VisibleForTesting
+ abstract Builder setNow(ZonedDateTime now);
+
+ abstract ScheduleConfig build();
+
+ public Optional<Schedule> buildSchedule() {
+ return build().schedule();
+ }
+ }
+
+ @AutoValue
+ public abstract static class Schedule {
+ /** Number of milliseconds between events. */
+ public abstract long interval();
+
+ /**
+ * Milliseconds between constructor invocation and first event time.
+ *
+ * <p>If there is any lag between the constructor invocation and queuing the object into an
+ * executor the event will run later, as there is no method to adjust for the scheduling delay.
+ */
+ public abstract long initialDelay();
+
+ /**
+ * Creates a schedule.
+ *
+ * <p>{@link ScheduleConfig} defines details about which values are valid for the {@code
+ * interval} and {@code startTime} parameters.
+ *
+ * @param interval the interval in milliseconds
+ * @param startTime the start time as "{@code <day of week> <hours>:<minutes>}" or "{@code
+ * <hours>:<minutes>}"
+ * @return the schedule
+ * @throws IllegalArgumentException if any of the parameters is invalid
+ */
+ public static Schedule createOrFail(long interval, String startTime) {
+ return create(interval, startTime).orElseThrow(IllegalArgumentException::new);
+ }
+
+ /**
+ * Creates a schedule.
+ *
+ * <p>{@link ScheduleConfig} defines details about which values are valid for the {@code
+ * interval} and {@code startTime} parameters.
+ *
+ * @param interval the interval in milliseconds
+ * @param startTime the start time as "{@code <day of week> <hours>:<minutes>}" or "{@code
+ * <hours>:<minutes>}"
+ * @return the schedule or {@link Optional#empty()} if any of the parameters is invalid
+ */
+ public static Optional<Schedule> create(long interval, String startTime) {
+ long initialDelay = computeInitialDelay(interval, startTime);
+ if (interval <= 0 || initialDelay < 0) {
+ return Optional.empty();
+ }
+ return Optional.of(create(interval, initialDelay));
+ }
+
+ static Schedule create(long interval, long initialDelay) {
+ return new AutoValue_ScheduleConfig_Schedule(interval, initialDelay);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
new file mode 100644
index 0000000..022b0e1
--- /dev/null
+++ b/java/com/google/gerrit/server/git/DefaultAdvertiseRefsHook.java
@@ -0,0 +1,52 @@
+// 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.git;
+
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import java.util.Map;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.AbstractAdvertiseRefsHook;
+import org.eclipse.jgit.transport.ServiceMayNotContinueException;
+
+/**
+ * Wrapper around {@link com.google.gerrit.server.permissions.PermissionBackend.ForProject} that
+ * implements {@link org.eclipse.jgit.transport.AdvertiseRefsHook}.
+ */
+public class DefaultAdvertiseRefsHook extends AbstractAdvertiseRefsHook {
+
+ private final PermissionBackend.ForProject perm;
+ private final PermissionBackend.RefFilterOptions opts;
+
+ public DefaultAdvertiseRefsHook(
+ PermissionBackend.ForProject perm, PermissionBackend.RefFilterOptions opts) {
+ this.perm = perm;
+ this.opts = opts;
+ }
+
+ @Override
+ protected Map<String, Ref> getAdvertisedRefs(Repository repo, RevWalk revWalk)
+ throws ServiceMayNotContinueException {
+ try {
+ return perm.filter(repo.getAllRefs(), repo, opts);
+ } catch (PermissionBackendException e) {
+ ServiceMayNotContinueException ex = new ServiceMayNotContinueException();
+ ex.initCause(e);
+ throw ex;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/git/GarbageCollectionRunner.java b/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
index ea93f96..e4316c5 100644
--- a/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
+++ b/java/com/google/gerrit/server/git/GarbageCollectionRunner.java
@@ -14,23 +14,17 @@
package com.google.gerrit.server.git;
-import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG;
-
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.server.config.GcConfig;
-import com.google.gerrit.server.config.ScheduleConfig;
import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Runnable to enable scheduling gc to run periodically */
public class GarbageCollectionRunner implements Runnable {
private static final Logger gcLog = LoggerFactory.getLogger(GarbageCollection.LOG_NAME);
- private static final Logger log = LoggerFactory.getLogger(GarbageCollectionRunner.class);
static class Lifecycle implements LifecycleListener {
private final WorkQueue queue;
@@ -46,20 +40,7 @@
@Override
public void start() {
- ScheduleConfig scheduleConfig = gcConfig.getScheduleConfig();
- long interval = scheduleConfig.getInterval();
- long delay = scheduleConfig.getInitialDelay();
- if (delay == MISSING_CONFIG && interval == MISSING_CONFIG) {
- log.info("Ignoring missing gc schedule configuration");
- } else if (delay < 0 || interval <= 0) {
- log.warn(String.format("Ignoring invalid gc schedule configuration: %s", scheduleConfig));
- } else {
- @SuppressWarnings("unused")
- Future<?> possiblyIgnoredError =
- queue
- .getDefaultQueue()
- .scheduleAtFixedRate(gcRunner, delay, interval, TimeUnit.MILLISECONDS);
- }
+ gcConfig.getSchedule().ifPresent(s -> queue.scheduleAtFixedRate(gcRunner, s));
}
@Override
diff --git a/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java b/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
index cee08d4..938f6cf 100644
--- a/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
+++ b/java/com/google/gerrit/server/git/MergeIdenticalTreeException.java
@@ -14,13 +14,13 @@
package com.google.gerrit.server.git;
-import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
/**
* Indicates that the commit is already contained in destination branch. Either the commit itself is
* in the source tree, or the content is merged
*/
-public class MergeIdenticalTreeException extends RestApiException {
+public class MergeIdenticalTreeException extends ResourceConflictException {
private static final long serialVersionUID = 1L;
/** @param msg message to return to the client describing the error. */
diff --git a/java/com/google/gerrit/server/git/MergeOp.java b/java/com/google/gerrit/server/git/MergeOp.java
index 114e1da..b4a66f4 100644
--- a/java/com/google/gerrit/server/git/MergeOp.java
+++ b/java/com/google/gerrit/server/git/MergeOp.java
@@ -328,8 +328,7 @@
return allowClosed ? SUBMIT_RULE_OPTIONS_ALLOW_CLOSED : SUBMIT_RULE_OPTIONS;
}
- private static List<SubmitRecord> getSubmitRecords(ChangeData cd, boolean allowClosed)
- throws OrmException {
+ private static List<SubmitRecord> getSubmitRecords(ChangeData cd, boolean allowClosed) {
return cd.submitRecords(submitRuleOptions(allowClosed));
}
@@ -393,7 +392,7 @@
commitStatus.maybeFailVerbose();
}
- private void bypassSubmitRules(ChangeSet cs, boolean allowClosed) throws OrmException {
+ private void bypassSubmitRules(ChangeSet cs, boolean allowClosed) {
checkArgument(
!cs.furtherHiddenChanges(), "cannot bypass submit rules for topic with hidden change");
for (ChangeData cd : cs.changes()) {
@@ -834,7 +833,7 @@
}
}
- private SubmitType getSubmitType(ChangeData cd) throws OrmException {
+ private SubmitType getSubmitType(ChangeData cd) {
SubmitTypeRecord str = cd.submitTypeRecord();
return str.isOk() ? str.type : null;
}
diff --git a/java/com/google/gerrit/server/git/ProjectConfig.java b/java/com/google/gerrit/server/git/ProjectConfig.java
index abef5a8..2dc01b4 100644
--- a/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -154,6 +154,8 @@
private static final String EXTENSION_PANELS = "extension-panels";
private static final String KEY_PANEL = "panel";
+ private static final Pattern EXCLUSIVE_PERMISSIONS_SPLIT_PATTERN = Pattern.compile("[, \t]{1,}");
+
private Project.NameKey projectName;
private Project project;
private AccountsSection accountsSection;
@@ -678,7 +680,7 @@
AccessSection as = getAccessSection(refName, true);
for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) {
- for (String n : varName.split("[, \t]{1,}")) {
+ for (String n : Splitter.on(EXCLUSIVE_PERMISSIONS_SPLIT_PATTERN).split(varName)) {
n = convertLegacyPermission(n);
if (isPermission(n)) {
as.getPermission(n, true).setExclusiveGroup(true);
diff --git a/java/com/google/gerrit/server/git/TagCache.java b/java/com/google/gerrit/server/git/TagCache.java
index 0822161..d346997 100644
--- a/java/com/google/gerrit/server/git/TagCache.java
+++ b/java/com/google/gerrit/server/git/TagCache.java
@@ -82,7 +82,7 @@
}
}
- TagSetHolder get(Project.NameKey name) {
+ public TagSetHolder get(Project.NameKey name) {
EntryVal val = cache.getIfPresent(name.get());
if (val == null) {
synchronized (createLock) {
diff --git a/java/com/google/gerrit/server/git/TagMatcher.java b/java/com/google/gerrit/server/git/TagMatcher.java
index 6e46d76..945e91e 100644
--- a/java/com/google/gerrit/server/git/TagMatcher.java
+++ b/java/com/google/gerrit/server/git/TagMatcher.java
@@ -23,7 +23,7 @@
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
-class TagMatcher {
+public class TagMatcher {
final BitSet mask = new BitSet();
final List<Ref> newRefs = new ArrayList<>();
final List<LostRef> lostRefs = new ArrayList<>();
@@ -50,7 +50,7 @@
this.updated = updated;
}
- boolean isReachable(Ref tagRef) {
+ public boolean isReachable(Ref tagRef) {
tagRef = db.peel(tagRef);
ObjectId tagObj = tagRef.getPeeledObjectId();
diff --git a/java/com/google/gerrit/server/git/TagSetHolder.java b/java/com/google/gerrit/server/git/TagSetHolder.java
index e1faa65..3f08d10 100644
--- a/java/com/google/gerrit/server/git/TagSetHolder.java
+++ b/java/com/google/gerrit/server/git/TagSetHolder.java
@@ -21,7 +21,7 @@
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
-class TagSetHolder {
+public class TagSetHolder {
private final Object buildLock = new Object();
private final Project.NameKey projectName;
private volatile TagSet tags;
@@ -42,7 +42,7 @@
this.tags = tags;
}
- TagMatcher matcher(TagCache cache, Repository db, Collection<Ref> include) {
+ public TagMatcher matcher(TagCache cache, Repository db, Collection<Ref> include) {
include = include.stream().filter(r -> !TagSet.skip(r)).collect(toList());
TagSet tags = this.tags;
diff --git a/java/com/google/gerrit/server/git/WorkQueue.java b/java/com/google/gerrit/server/git/WorkQueue.java
index 0adb45a..11e149c 100644
--- a/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/java/com/google/gerrit/server/git/WorkQueue.java
@@ -18,6 +18,7 @@
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ScheduleConfig.Schedule;
import com.google.gerrit.server.util.IdGenerator;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -130,6 +131,15 @@
return executor;
}
+ /** Executes a periodic command at a fixed schedule on the default queue. */
+ public void scheduleAtFixedRate(Runnable command, Schedule schedule) {
+ @SuppressWarnings("unused")
+ Future<?> possiblyIgnoredError =
+ getDefaultQueue()
+ .scheduleAtFixedRate(
+ command, schedule.initialDelay(), schedule.interval(), TimeUnit.MILLISECONDS);
+ }
+
/** Get all of the tasks currently scheduled in any work queue. */
public List<Task<?>> getTasks() {
final List<Task<?>> r = new ArrayList<>();
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 6eba282..597bce1 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -23,13 +23,14 @@
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.git.DefaultAdvertiseRefsHook;
import com.google.gerrit.server.git.HackPushNegotiateHook;
import com.google.gerrit.server.git.MultiProgressMonitor;
import com.google.gerrit.server.git.ProjectRunnable;
import com.google.gerrit.server.git.TransferConfig;
-import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.project.ContributorAgreementsChecker;
@@ -183,7 +184,6 @@
AsyncReceiveCommits(
ReceiveCommits.Factory factory,
PermissionBackend permissionBackend,
- VisibleRefFilter.Factory refFilterFactory,
Provider<InternalChangeQuery> queryProvider,
@ReceiveCommitsExecutor ExecutorService executor,
RequestScopePropagator scopePropagator,
@@ -236,7 +236,8 @@
List<AdvertiseRefsHook> advHooks = new ArrayList<>(4);
allRefsWatcher = new AllRefsWatcher();
advHooks.add(allRefsWatcher);
- advHooks.add(refFilterFactory.create(projectState, repo).setShowMetadata(false));
+ advHooks.add(
+ new DefaultAdvertiseRefsHook(perm, RefFilterOptions.builder().setFilterMeta(true).build()));
advHooks.add(new ReceiveCommitsAdvertiseRefsHook(queryProvider, projectName));
advHooks.add(new HackPushNegotiateHook());
rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index caf3a91..0e54e59 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -838,6 +838,11 @@
continue;
}
+ if (!projectState.getProject().getState().permitsWrite()) {
+ reject(cmd, "prohibited by Gerrit: project state does not permit write");
+ return;
+ }
+
if (MagicBranch.isMagicBranch(cmd.getRefName())) {
parseMagicBranch(cmd);
continue;
@@ -1070,9 +1075,6 @@
} catch (AuthException err) {
ok = false;
}
- if (!projectState.statePermitsWrite()) {
- reject(cmd, "prohibited by Gerrit: project state does not permit write");
- }
if (ok) {
if (isHead(cmd) && !isCommit(cmd)) {
return;
@@ -1169,9 +1171,6 @@
if (!validRefOperation(cmd)) {
return;
}
- if (!projectState.statePermitsWrite()) {
- cmd.setResult(REJECTED_NONFASTFORWARD, " project state does not permit write.");
- }
actualCommands.add(cmd);
} else {
cmd.setResult(
@@ -1519,10 +1518,6 @@
magicBranch.dest = new Branch.NameKey(project.getNameKey(), ref);
magicBranch.perm = permissions.ref(ref);
- if (!projectState.getProject().getState().permitsWrite()) {
- reject(cmd, "project state does not permit write");
- return;
- }
try {
magicBranch.perm.check(RefPermission.CREATE_CHANGE);
@@ -1570,10 +1565,6 @@
reject(cmd, e.getMessage());
return;
}
- if (!projectState.statePermitsWrite()) {
- reject(cmd, "project state does not permit write");
- return;
- }
}
RevWalk walk = rp.getRevWalk();
diff --git a/java/com/google/gerrit/server/git/validators/AccountValidator.java b/java/com/google/gerrit/server/git/validators/AccountValidator.java
index 4661987..5462631 100644
--- a/java/com/google/gerrit/server/git/validators/AccountValidator.java
+++ b/java/com/google/gerrit/server/git/validators/AccountValidator.java
@@ -101,7 +101,7 @@
throws IOException, ConfigInvalidException {
rw.reset();
AccountConfig accountConfig = new AccountConfig(accountId, repo);
- accountConfig.setEagerParsing(true).load(rw, commit);
+ accountConfig.load(rw, commit);
if (messages != null) {
messages.addAll(
accountConfig
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index d5db92f..632eb47 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -20,7 +20,9 @@
import static java.util.stream.Collectors.toList;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.PageLinks;
@@ -225,6 +227,7 @@
messages.addAll(commitValidator.onCommitReceived(receiveEvent));
}
} catch (CommitValidationException e) {
+ log.debug("CommitValidationException occurred: {}", e.getFullMessage(), e);
// Keep the old messages (and their order) in case of an exception
messages.addAll(e.getMessages());
throw new CommitValidationException(e.getMessage(), messages);
@@ -333,9 +336,7 @@
sb.append("ERROR: ").append(errMsg);
if (c.getFullMessage().indexOf(CHANGE_ID_PREFIX) >= 0) {
- String[] lines = c.getFullMessage().trim().split("\n");
- String lastLine = lines.length > 0 ? lines[lines.length - 1] : "";
-
+ String lastLine = Iterables.getLast(Splitter.on('\n').split(c.getFullMessage()), "");
if (lastLine.indexOf(CHANGE_ID_PREFIX) == -1) {
sb.append('\n');
sb.append('\n');
diff --git a/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
new file mode 100644
index 0000000..d30945f
--- /dev/null
+++ b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
@@ -0,0 +1,154 @@
+// 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;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.ScheduleConfig;
+import com.google.gerrit.server.config.ScheduleConfig.Schedule;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.group.db.GroupNameNotes;
+import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Runnable to schedule periodic group reindexing.
+ *
+ * <p>Periodic group indexing is intended to run only on slaves. Replication to slaves happens on
+ * Git level so that Gerrit is not aware of incoming replication events. But slaves need an updated
+ * group index to resolve memberships of users for ACL validation. To keep the group index in slaves
+ * up-to-date this class periodically scans the group refs in the All-Users repository to reindex
+ * groups if they are stale. The ref states of the group refs are cached so that on each run deleted
+ * groups can be detected and reindexed. This means callers of slaves may observe outdated group
+ * information until the next indexing happens. The interval on which group indexing is done is
+ * configurable by setting {@code index.scheduledIndexer.interval} in {@code gerrit.config}. By
+ * default group indexing is done every 5 minutes.
+ *
+ * <p>This class is not able to detect group deletions that were replicated while the slave was
+ * offline. This means if group refs are deleted while the slave is offline these groups are not
+ * removed from the group index when the slave is started. However since group deletion is not
+ * supported this should never happen and one can always do an offline reindex before starting the
+ * slave.
+ */
+public class PeriodicGroupIndexer implements Runnable {
+ private static final Logger log = LoggerFactory.getLogger(PeriodicGroupIndexer.class);
+
+ public static class Module extends LifecycleModule {
+ @Override
+ protected void configure() {
+ listener().to(Lifecycle.class);
+ }
+ }
+
+ private static class Lifecycle implements LifecycleListener {
+ private final Config cfg;
+ private final WorkQueue queue;
+ private final PeriodicGroupIndexer runner;
+
+ @Inject
+ Lifecycle(@GerritServerConfig Config cfg, WorkQueue queue, PeriodicGroupIndexer runner) {
+ this.cfg = cfg;
+ this.queue = queue;
+ this.runner = runner;
+ }
+
+ @Override
+ public void start() {
+ boolean runOnStartup = cfg.getBoolean("index", "scheduledIndexer", "runOnStartup", true);
+ if (runOnStartup) {
+ runner.run();
+ }
+
+ boolean isEnabled = cfg.getBoolean("index", "scheduledIndexer", "enabled", true);
+ if (!isEnabled) {
+ log.warn("index.scheduledIndexer is disabled");
+ return;
+ }
+
+ Schedule schedule =
+ ScheduleConfig.builder(cfg, "index")
+ .setSubsection("scheduledIndexer")
+ .buildSchedule()
+ .orElseGet(() -> Schedule.createOrFail(TimeUnit.MINUTES.toMillis(5), "00:00"));
+ queue.scheduleAtFixedRate(runner, schedule);
+ }
+
+ @Override
+ public void stop() {
+ // handled by WorkQueue.stop() already
+ }
+ }
+
+ private final AllUsersName allUsersName;
+ private final GitRepositoryManager repoManager;
+ private final Provider<GroupIndexer> groupIndexerProvider;
+
+ private ImmutableSet<AccountGroup.UUID> groupUuids;
+
+ @Inject
+ PeriodicGroupIndexer(
+ AllUsersName allUsersName,
+ GitRepositoryManager repoManager,
+ Provider<GroupIndexer> groupIndexerProvider) {
+ this.allUsersName = allUsersName;
+ this.repoManager = repoManager;
+ this.groupIndexerProvider = groupIndexerProvider;
+ }
+
+ @Override
+ public synchronized void run() {
+ try (Repository allUsers = repoManager.openRepository(allUsersName)) {
+ ImmutableSet<AccountGroup.UUID> newGroupUuids =
+ GroupNameNotes.loadAllGroups(allUsers)
+ .stream()
+ .map(GroupReference::getUUID)
+ .collect(toImmutableSet());
+ GroupIndexer groupIndexer = groupIndexerProvider.get();
+ int reindexCounter = 0;
+ for (AccountGroup.UUID groupUuid : newGroupUuids) {
+ if (groupIndexer.reindexIfStale(groupUuid)) {
+ reindexCounter++;
+ }
+ }
+ if (groupUuids != null) {
+ // Check if any group was deleted since the last run and if yes remove these groups from the
+ // index.
+ for (AccountGroup.UUID groupUuid : Sets.difference(groupUuids, newGroupUuids)) {
+ groupIndexer.index(groupUuid);
+ reindexCounter++;
+ }
+ }
+ groupUuids = newGroupUuids;
+ log.info("Run group indexer, {} groups reindexed", reindexCounter);
+ } catch (Throwable t) {
+ log.error("Failed to reindex groups", t);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/group/db/Groups.java b/java/com/google/gerrit/server/group/db/Groups.java
index d0ea9ca..bbdc8d2 100644
--- a/java/com/google/gerrit/server/group/db/Groups.java
+++ b/java/com/google/gerrit/server/group/db/Groups.java
@@ -78,23 +78,6 @@
}
/**
- * Returns the {@code AccountGroup} for the specified ID if it exists.
- *
- * @param db the {@code ReviewDb} instance to use for lookups
- * @param groupId the ID of the group
- * @return the found {@code AccountGroup} if it exists, or else an empty {@code Optional}
- * @throws OrmException if the group couldn't be retrieved from ReviewDb
- */
- public static Optional<InternalGroup> getGroupFromReviewDb(ReviewDb db, AccountGroup.Id groupId)
- throws OrmException {
- AccountGroup accountGroup = db.accountGroups().get(groupId);
- if (accountGroup == null) {
- return Optional.empty();
- }
- return Optional.of(asInternalGroup(db, accountGroup));
- }
-
- /**
* Returns the {@code InternalGroup} for the specified UUID if it exists.
*
* @param db the {@code ReviewDb} instance to use for lookups
@@ -210,7 +193,7 @@
* @return a stream of the IDs of the members
* @throws OrmException if an error occurs while reading from ReviewDb
*/
- public static Stream<Account.Id> getMembersFromReviewDb(ReviewDb db, AccountGroup.Id groupId)
+ static Stream<Account.Id> getMembersFromReviewDb(ReviewDb db, AccountGroup.Id groupId)
throws OrmException {
ResultSet<AccountGroupMember> accountGroupMembers = db.accountGroupMembers().byGroup(groupId);
return Streams.stream(accountGroupMembers).map(AccountGroupMember::getAccountId);
@@ -229,52 +212,13 @@
* @return a stream of the UUIDs of the subgroups
* @throws OrmException if an error occurs while reading from ReviewDb
*/
- public static Stream<AccountGroup.UUID> getSubgroupsFromReviewDb(
- ReviewDb db, AccountGroup.Id groupId) throws OrmException {
+ static Stream<AccountGroup.UUID> getSubgroupsFromReviewDb(ReviewDb db, AccountGroup.Id groupId)
+ throws OrmException {
ResultSet<AccountGroupById> accountGroupByIds = db.accountGroupById().byGroup(groupId);
return Streams.stream(accountGroupByIds).map(AccountGroupById::getIncludeUUID).distinct();
}
/**
- * Returns the groups of which the specified account is a member.
- *
- * <p><strong>Note</strong>: This method returns an empty stream if the account doesn't exist.
- * This method doesn't check whether the groups exist.
- *
- * @param db the {@code ReviewDb} instance to use for lookups
- * @param accountId the ID of the account
- * @return a stream of the IDs of the groups of which the account is a member
- * @throws OrmException if an error occurs while reading from ReviewDb
- */
- public static Stream<AccountGroup.Id> getGroupsWithMemberFromReviewDb(
- ReviewDb db, Account.Id accountId) throws OrmException {
- ResultSet<AccountGroupMember> accountGroupMembers =
- db.accountGroupMembers().byAccount(accountId);
- return Streams.stream(accountGroupMembers).map(AccountGroupMember::getAccountGroupId);
- }
-
- /**
- * Returns the parent groups of the specified (sub)group.
- *
- * <p>The subgroup may either be an internal or an external group whereas the returned parent
- * groups represent only internal groups.
- *
- * <p><strong>Note</strong>: This method returns an empty stream if the specified group doesn't
- * exist. This method doesn't check whether the parent groups exist.
- *
- * @param db the {@code ReviewDb} instance to use for lookups
- * @param subgroupUuid the UUID of the subgroup
- * @return a stream of the IDs of the parent groups
- * @throws OrmException if an error occurs while reading from ReviewDb
- */
- public static Stream<AccountGroup.Id> getParentGroupsFromReviewDb(
- ReviewDb db, AccountGroup.UUID subgroupUuid) throws OrmException {
- ResultSet<AccountGroupById> accountGroupByIds =
- db.accountGroupById().byIncludeUUID(subgroupUuid);
- return Streams.stream(accountGroupByIds).map(AccountGroupById::getGroupId);
- }
-
- /**
* Returns all known external groups. External groups are 'known' when they are specified as a
* subgroup of an internal group.
*
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index f52ea23..a3aae83 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -51,12 +51,14 @@
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.RenameGroupOp;
import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.index.group.GroupIndexer;
import com.google.gerrit.server.notedb.GroupsMigration;
import com.google.gerrit.server.update.RefUpdateUtil;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.sql.Timestamp;
@@ -102,6 +104,7 @@
private final AllUsersName allUsersName;
private final GroupCache groupCache;
private final GroupIncludeCache groupIncludeCache;
+ private final Provider<GroupIndexer> indexer;
private final AuditService auditService;
private final RenameGroupOp.Factory renameGroupOpFactory;
@Nullable private final IdentifiedUser currentUser;
@@ -120,6 +123,7 @@
GroupBackend groupBackend,
GroupCache groupCache,
GroupIncludeCache groupIncludeCache,
+ Provider<GroupIndexer> indexer,
AuditService auditService,
AccountCache accountCache,
RenameGroupOp.Factory renameGroupOpFactory,
@@ -135,6 +139,7 @@
this.allUsersName = allUsersName;
this.groupCache = groupCache;
this.groupIncludeCache = groupIncludeCache;
+ this.indexer = indexer;
this.auditService = auditService;
this.renameGroupOpFactory = renameGroupOpFactory;
this.groupsMigration = groupsMigration;
@@ -476,7 +481,8 @@
}
}
- private InternalGroup createGroupInNoteDb(
+ @VisibleForTesting
+ public InternalGroup createGroupInNoteDb(
InternalGroupCreation groupCreation, InternalGroupUpdate groupUpdate)
throws IOException, ConfigInvalidException, OrmDuplicateKeyException {
try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
@@ -595,7 +601,7 @@
}
private void updateCachesOnGroupCreation(InternalGroup createdGroup) throws IOException {
- groupCache.onCreateGroup(createdGroup.getGroupUUID());
+ indexer.get().index(createdGroup.getGroupUUID());
for (Account.Id modifiedMember : createdGroup.getMembers()) {
groupIncludeCache.evictGroupsWithMember(modifiedMember);
}
@@ -607,7 +613,7 @@
private void updateCachesOnGroupUpdate(UpdateResult result) throws IOException {
if (result.getPreviousGroupName().isPresent()) {
AccountGroup.NameKey previousName = result.getPreviousGroupName().get();
- groupCache.evictAfterRename(previousName);
+ groupCache.evict(previousName);
// TODO(aliceks): After switching to NoteDb, consider to use a BatchRefUpdate.
@SuppressWarnings("unused")
@@ -620,7 +626,10 @@
result.getGroupName().get())
.start(0, TimeUnit.MILLISECONDS);
}
- groupCache.evict(result.getGroupUuid(), result.getGroupId(), result.getGroupName());
+ groupCache.evict(result.getGroupUuid());
+ groupCache.evict(result.getGroupId());
+ groupCache.evict(result.getGroupName());
+ indexer.get().index(result.getGroupUuid());
result.getAddedMembers().forEach(groupIncludeCache::evictGroupsWithMember);
result.getDeletedMembers().forEach(groupIncludeCache::evictGroupsWithMember);
diff --git a/java/com/google/gerrit/server/index/DummyIndexModule.java b/java/com/google/gerrit/server/index/DummyIndexModule.java
index 85d6a7c..211f9d1 100644
--- a/java/com/google/gerrit/server/index/DummyIndexModule.java
+++ b/java/com/google/gerrit/server/index/DummyIndexModule.java
@@ -59,7 +59,7 @@
@Override
protected void configure() {
- install(new IndexModule(1));
+ install(new IndexModule(1, true));
bind(IndexConfig.class).toInstance(IndexConfig.createDefault());
bind(Index.class).toInstance(new DummyChangeIndex());
bind(AccountIndex.Factory.class).toInstance(new DummyAccountIndexFactory());
diff --git a/java/com/google/gerrit/server/index/IndexModule.java b/java/com/google/gerrit/server/index/IndexModule.java
index cb6dc3c..0c4a988 100644
--- a/java/com/google/gerrit/server/index/IndexModule.java
+++ b/java/com/google/gerrit/server/index/IndexModule.java
@@ -92,9 +92,11 @@
private final ListeningExecutorService interactiveExecutor;
private final ListeningExecutorService batchExecutor;
private final boolean closeExecutorsOnShutdown;
+ private final boolean slave;
- public IndexModule(int threads) {
+ public IndexModule(int threads, boolean slave) {
this.threads = threads;
+ this.slave = slave;
this.interactiveExecutor = null;
this.batchExecutor = null;
this.closeExecutorsOnShutdown = true;
@@ -106,6 +108,7 @@
this.interactiveExecutor = interactiveExecutor;
this.batchExecutor = batchExecutor;
this.closeExecutorsOnShutdown = false;
+ slave = false;
}
@Override
@@ -151,6 +154,11 @@
ChangeIndexDefinition changes,
GroupIndexDefinition groups,
ProjectIndexDefinition projects) {
+ if (slave) {
+ // In slave mode, we only have the group index.
+ return ImmutableList.of(groups);
+ }
+
Collection<IndexDefinition<?, ?, ?>> result =
ImmutableList.<IndexDefinition<?, ?, ?>>of(accounts, groups, changes, projects);
Set<String> expected =
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexer.java b/java/com/google/gerrit/server/index/account/AccountIndexer.java
index dd24714..91fa1d9 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexer.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexer.java
@@ -25,4 +25,12 @@
* @param id account id to index.
*/
void index(Account.Id id) throws IOException;
+
+ /**
+ * Synchronously reindex an account if it is stale.
+ *
+ * @param id account id to index.
+ * @return whether the account was reindexed
+ */
+ boolean reindexIfStale(Account.Id id) throws IOException;
}
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
index 8ca4a34..d055a46 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexerImpl.java
@@ -35,7 +35,6 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
-import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import org.eclipse.jgit.lib.Config;
@@ -102,6 +101,15 @@
autoReindexIfStale(id);
}
+ @Override
+ public boolean reindexIfStale(Account.Id id) throws IOException {
+ if (stalenessChecker.isStale(id)) {
+ index(id);
+ return true;
+ }
+ return false;
+ }
+
private static boolean autoReindexIfStale(Config cfg) {
return cfg.getBoolean("index", null, "autoReindexIfStale", true);
}
@@ -110,7 +118,7 @@
if (autoReindexIfStale) {
// Don't retry indefinitely; if this fails the account will be stale.
@SuppressWarnings("unused")
- Future<?> possiblyIgnoredError = reindexIfStale(id);
+ Future<?> possiblyIgnoredError = reindexIfStaleAsync(id);
}
}
@@ -124,19 +132,11 @@
* @return future for reindexing the account; returns true if the account was stale.
*/
@SuppressWarnings("deprecation")
- public com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStale(
+ private com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStaleAsync(
Account.Id id) {
- Callable<Boolean> task =
- () -> {
- if (stalenessChecker.isStale(id)) {
- index(id);
- return true;
- }
- return false;
- };
-
return Futures.makeChecked(
- Futures.nonCancellationPropagating(batchExecutor.submit(task)), IndexUtils.MAPPER);
+ Futures.nonCancellationPropagating(batchExecutor.submit(() -> reindexIfStale(id))),
+ IndexUtils.MAPPER);
}
private void fireAccountIndexedEvent(int id) {
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 1fbe3f0..344048f 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -256,39 +256,49 @@
return state.toString() + ',' + adr;
}
- public static ReviewerSet parseReviewerFieldValues(Iterable<String> values) {
+ public static ReviewerSet parseReviewerFieldValues(Change.Id changeId, Iterable<String> values) {
ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Timestamp> b =
ImmutableTable.builder();
for (String v : values) {
int i = v.indexOf(',');
if (i < 0) {
- log.error("Invalid value for reviewer field: %s", v);
+ log.warn("Invalid value for reviewer field from change {}: {}", changeId.get(), v);
continue;
}
int i2 = v.lastIndexOf(',');
if (i2 == i) {
- log.error("Invalid value for reviewer field: %s", v);
+ // Don't log a warning here.
+ // For each reviewer we store 2 values in the reviewer field, one value with the format
+ // "<reviewer-type>,<account-id>" and one value with the format
+ // "<reviewer-type>,<account-id>,<timestamp>" (see #getReviewerFieldValues(ReviewerSet)).
+ // For parsing we are only interested in the "<reviewer-type>,<account-id>,<timestamp>"
+ // value and the "<reviewer-type>,<account-id>" value is ignored here.
continue;
}
com.google.common.base.Optional<ReviewerStateInternal> reviewerState =
Enums.getIfPresent(ReviewerStateInternal.class, v.substring(0, i));
if (!reviewerState.isPresent()) {
- log.error("Failed to parse reviewer state from reviewer field: %s", v);
+ log.warn(
+ "Failed to parse reviewer state of reviewer field from change {}: {}",
+ changeId.get(),
+ v);
continue;
}
Optional<Account.Id> accountId = Account.Id.tryParse(v.substring(i + 1, i2));
if (!accountId.isPresent()) {
- log.error("Failed to parse account ID from reviewer field: %s", v);
+ log.warn(
+ "Failed to parse account ID of reviewer field from change {}: {}", changeId.get(), v);
continue;
}
Long l = Longs.tryParse(v.substring(i2 + 1, v.length()));
if (l == null) {
- log.error("Failed to parse timestamp from reviewer field: %s", v);
+ log.warn(
+ "Failed to parse timestamp of reviewer field from change {}: {}", changeId.get(), v);
continue;
}
Timestamp timestamp = new Timestamp(l);
@@ -298,37 +308,53 @@
return ReviewerSet.fromTable(b.build());
}
- public static ReviewerByEmailSet parseReviewerByEmailFieldValues(Iterable<String> values) {
+ public static ReviewerByEmailSet parseReviewerByEmailFieldValues(
+ Change.Id changeId, Iterable<String> values) {
ImmutableTable.Builder<ReviewerStateInternal, Address, Timestamp> b = ImmutableTable.builder();
for (String v : values) {
int i = v.indexOf(',');
if (i < 0) {
- log.error("Invalid value for reviewer by email field: %s", v);
+ log.warn("Invalid value for reviewer by email field from change {}: {}", changeId.get(), v);
continue;
}
int i2 = v.lastIndexOf(',');
if (i2 == i) {
- log.error("Invalid value for reviewer by email field: %s", v);
+ // Don't log a warning here.
+ // For each reviewer we store 2 values in the reviewer field, one value with the format
+ // "<reviewer-type>,<email>" and one value with the format
+ // "<reviewer-type>,<email>,<timestamp>" (see
+ // #getReviewerByEmailFieldValues(ReviewerByEmailSet)).
+ // For parsing we are only interested in the "<reviewer-type>,<email>,<timestamp>" value
+ // and the "<reviewer-type>,<email>" value is ignored here.
continue;
}
com.google.common.base.Optional<ReviewerStateInternal> reviewerState =
Enums.getIfPresent(ReviewerStateInternal.class, v.substring(0, i));
if (!reviewerState.isPresent()) {
- log.error("Failed to parse reviewer state from reviewer by email field: %s", v);
+ log.warn(
+ "Failed to parse reviewer state of reviewer by email field from change {}: {}",
+ changeId.get(),
+ v);
continue;
}
Address address = Address.tryParse(v.substring(i + 1, i2));
if (address == null) {
- log.error("Failed to parse address from reviewer by email field: %s", v);
+ log.warn(
+ "Failed to parse address of reviewer by email field from change {}: {}",
+ changeId.get(),
+ v);
continue;
}
Long l = Longs.tryParse(v.substring(i2 + 1, v.length()));
if (l == null) {
- log.error("Failed to parse timestamp from reviewer by email field: %s", v);
+ log.warn(
+ "Failed to parse timestamp of reviewer by email field from change {}: {}",
+ changeId.get(),
+ v);
continue;
}
Timestamp timestamp = new Timestamp(l);
@@ -703,8 +729,7 @@
return Lists.transform(records, r -> GSON.toJson(new StoredSubmitRecord(r)).getBytes(UTF_8));
}
- private static Iterable<byte[]> storedSubmitRecords(ChangeData cd, SubmitRuleOptions opts)
- throws OrmException {
+ private static Iterable<byte[]> storedSubmitRecords(ChangeData cd, SubmitRuleOptions opts) {
return storedSubmitRecords(cd.submitRecords(opts));
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
index e95470d..cf51197 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexer.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -34,6 +34,7 @@
import com.google.gerrit.server.index.IndexUtils;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
@@ -53,6 +54,7 @@
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicReference;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -430,11 +432,23 @@
@Override
public Boolean callImpl(Provider<ReviewDb> db) throws Exception {
- if (!stalenessChecker.isStale(id)) {
- return false;
+ try {
+ if (stalenessChecker.isStale(id)) {
+ index(newChangeData(db.get(), project, id));
+ return true;
+ }
+ } catch (NoSuchChangeException nsce) {
+ log.debug("Change {} was deleted, aborting reindexing the change.", id.get());
+ } catch (Exception e) {
+ if (!isCausedByRepositoryNotFoundException(e)) {
+ throw e;
+ }
+ log.debug(
+ "Change {} belongs to deleted project {}, aborting reindexing the change.",
+ id.get(),
+ project.get());
}
- index(newChangeData(db.get(), project, id));
- return true;
+ return false;
}
@Override
@@ -443,6 +457,16 @@
}
}
+ private boolean isCausedByRepositoryNotFoundException(Throwable throwable) {
+ while (throwable != null) {
+ if (throwable instanceof RepositoryNotFoundException) {
+ return true;
+ }
+ throwable = throwable.getCause();
+ }
+ return false;
+ }
+
// Avoid auto-rebuilding when reindexing if reading is disabled. This just
// increases contention on the meta ref from a background indexing thread
// with little benefit. The next actual write to the entity may still incur a
diff --git a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
index aca4eab..47dad94 100644
--- a/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
+++ b/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -98,11 +98,7 @@
executor.submit(
() -> {
try {
- Optional<InternalGroup> oldGroup = groupCache.get(uuid);
- if (oldGroup.isPresent()) {
- InternalGroup group = oldGroup.get();
- groupCache.evict(group.getGroupUUID(), group.getId(), group.getNameKey());
- }
+ groupCache.evict(uuid);
Optional<InternalGroup> internalGroup = groupCache.get(uuid);
if (internalGroup.isPresent()) {
index.replace(internalGroup.get());
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexer.java b/java/com/google/gerrit/server/index/group/GroupIndexer.java
index 0925cf4..503fd6b 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexer.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexer.java
@@ -25,4 +25,12 @@
* @param uuid group UUID to index.
*/
void index(AccountGroup.UUID uuid) throws IOException;
+
+ /**
+ * Synchronously reindex a group if it is stale.
+ *
+ * @param uuid group UUID to index.
+ * @return whether the group was reindexed
+ */
+ boolean reindexIfStale(AccountGroup.UUID uuid) throws IOException;
}
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
index b101dcb..a1da4ea 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -35,7 +35,6 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
-import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import org.eclipse.jgit.lib.Config;
@@ -91,6 +90,8 @@
@Override
public void index(AccountGroup.UUID uuid) throws IOException {
for (Index<AccountGroup.UUID, InternalGroup> i : getWriteIndexes()) {
+ // Evict the cache to get an up-to-date value for sure.
+ groupCache.evict(uuid);
Optional<InternalGroup> internalGroup = groupCache.get(uuid);
if (internalGroup.isPresent()) {
i.replace(internalGroup.get());
@@ -102,6 +103,15 @@
autoReindexIfStale(uuid);
}
+ @Override
+ public boolean reindexIfStale(AccountGroup.UUID uuid) throws IOException {
+ if (stalenessChecker.isStale(uuid)) {
+ index(uuid);
+ return true;
+ }
+ return false;
+ }
+
private static boolean autoReindexIfStale(Config cfg) {
return cfg.getBoolean("index", null, "autoReindexIfStale", true);
}
@@ -110,7 +120,7 @@
if (autoReindexIfStale) {
// Don't retry indefinitely; if this fails the group will be stale.
@SuppressWarnings("unused")
- Future<?> possiblyIgnoredError = reindexIfStale(uuid);
+ Future<?> possiblyIgnoredError = reindexIfStaleAsync(uuid);
}
}
@@ -124,19 +134,11 @@
* @return future for reindexing the group; returns true if the group was stale.
*/
@SuppressWarnings("deprecation")
- public com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStale(
+ private com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStaleAsync(
AccountGroup.UUID uuid) {
- Callable<Boolean> task =
- () -> {
- if (stalenessChecker.isStale(uuid)) {
- index(uuid);
- return true;
- }
- return false;
- };
-
return Futures.makeChecked(
- Futures.nonCancellationPropagating(batchExecutor.submit(task)), IndexUtils.MAPPER);
+ Futures.nonCancellationPropagating(batchExecutor.submit(() -> reindexIfStale(uuid))),
+ IndexUtils.MAPPER);
}
private void fireGroupIndexedEvent(String uuid) {
diff --git a/java/com/google/gerrit/server/index/group/StalenessChecker.java b/java/com/google/gerrit/server/index/group/StalenessChecker.java
index 418bb35..94e1be7 100644
--- a/java/com/google/gerrit/server/index/group/StalenessChecker.java
+++ b/java/com/google/gerrit/server/index/group/StalenessChecker.java
@@ -72,9 +72,6 @@
if (i == null) {
return false; // No index; caller couldn't do anything if it is stale.
}
- if (!i.getSchema().hasField(GroupField.REF_STATE)) {
- return false; // Index version not new enough for this check.
- }
Optional<FieldBundle> result =
i.getRaw(uuid, IndexedGroupQuery.createOptions(indexConfig, 0, 1, FIELDS));
diff --git a/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java b/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java
new file mode 100644
index 0000000..481c2e9
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/AutoReplyMailFilter.java
@@ -0,0 +1,55 @@
+// 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.mail;
+
+import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.inject.Singleton;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Filters out auto-reply messages according to RFC 3834. */
+@Singleton
+public class AutoReplyMailFilter implements MailFilter {
+
+ private static final Logger log = LoggerFactory.getLogger(AutoReplyMailFilter.class);
+
+ @Override
+ public boolean shouldProcessMessage(MailMessage message) {
+ for (String header : message.additionalHeaders()) {
+ if (header.startsWith(MailHeader.PRECEDENCE.fieldWithDelimiter())) {
+ String prec = header.substring(MailHeader.PRECEDENCE.fieldWithDelimiter().length()).trim();
+
+ if (prec.equals("list") || prec.equals("junk") || prec.equals("bulk")) {
+ log.error(
+ "Message %s has a Precedence header. Will ignore and delete message.", message.id());
+ return false;
+ }
+
+ } else if (header.startsWith(MailHeader.AUTO_SUBMITTED.fieldWithDelimiter())) {
+ String autoSubmitted =
+ header.substring(MailHeader.AUTO_SUBMITTED.fieldWithDelimiter().length()).trim();
+
+ if (!autoSubmitted.equals("no")) {
+ log.error(
+ "Message %s has an Auto-Submitted header. Will ignore and delete message.",
+ message.id());
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/java/com/google/gerrit/server/mail/MailHeader.java b/java/com/google/gerrit/server/mail/MailHeader.java
new file mode 100644
index 0000000..be12288
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/MailHeader.java
@@ -0,0 +1,71 @@
+// 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.mail;
+
+/** Variables used by emails to hold data */
+public enum MailHeader {
+ // Gerrit metadata holders
+ ASSIGNEE("Gerrit-Assignee"),
+ BRANCH("Gerrit-Branch"),
+ CC("Gerit-CC"),
+ COMMENT_IN_REPLY_TO("Comment-In-Reply-To"),
+ COMMENT_DATE("Gerrit-Comment-Date"),
+ CHANGE_ID("Gerrit-Change-Id"),
+ CHANGE_NUMBER("Gerrit-Change-Number"),
+ CHANGE_URL("Gerrit-ChangeURL"),
+ COMMIT("Gerrit-Commit"),
+ HAS_COMMENTS("Gerrit-HasComments"),
+ HAS_LABELS("Gerrit-Has-Labels"),
+ MESSAGE_TYPE("Gerrit-MessageType"),
+ OWNER("Gerrit-Owner"),
+ PATCH_SET("Gerrit-PatchSet"),
+ PROJECT("Gerrit-Project"),
+ REVIEWER("Gerrit-Reviewer"),
+
+ // Commonly used Email headers
+ AUTO_SUBMITTED("Auto-Submitted"),
+ PRECEDENCE("Precedence"),
+ REFERENCES("References");
+
+ private final String name;
+ private final String fieldName;
+
+ MailHeader(String name) {
+ boolean customHeader = name.startsWith("Gerrit-");
+ this.name = name;
+
+ if (customHeader) {
+ this.fieldName = "X-" + name;
+ } else {
+ this.fieldName = name;
+ }
+ }
+
+ public String fieldWithDelimiter() {
+ return fieldName() + ": ";
+ }
+
+ public String withDelimiter() {
+ return name + ": ";
+ }
+
+ public String fieldName() {
+ return fieldName;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/java/com/google/gerrit/server/mail/MetadataName.java b/java/com/google/gerrit/server/mail/MetadataName.java
deleted file mode 100644
index 3080e4f..0000000
--- a/java/com/google/gerrit/server/mail/MetadataName.java
+++ /dev/null
@@ -1,34 +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.mail;
-
-public final class MetadataName {
- public static final String CHANGE_NUMBER = "Gerrit-Change-Number";
- public static final String PATCH_SET = "Gerrit-PatchSet";
- public static final String MESSAGE_TYPE = "Gerrit-MessageType";
- public static final String TIMESTAMP = "Gerrit-Comment-Date";
-
- public static String toHeader(String metadataName) {
- return "X-" + metadataName;
- }
-
- public static String toHeaderWithDelimiter(String metadataName) {
- return toHeader(metadataName) + ": ";
- }
-
- public static String toFooterWithDelimiter(String metadataName) {
- return metadataName + ": ";
- }
-}
diff --git a/java/com/google/gerrit/server/mail/receive/MailHeaderParser.java b/java/com/google/gerrit/server/mail/receive/MailHeaderParser.java
new file mode 100644
index 0000000..05525bd
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/receive/MailHeaderParser.java
@@ -0,0 +1,107 @@
+// 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.mail.receive;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.server.mail.MailHeader;
+import com.google.gerrit.server.mail.MailUtil;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.format.DateTimeParseException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Parse metadata from inbound email */
+public class MailHeaderParser {
+ private static final Logger log = LoggerFactory.getLogger(MailHeaderParser.class);
+
+ public static MailMetadata parse(MailMessage m) {
+ MailMetadata metadata = new MailMetadata();
+ // Find author
+ metadata.author = m.from().getEmail();
+
+ // Check email headers for X-Gerrit-<Name>
+ for (String header : m.additionalHeaders()) {
+ if (header.startsWith(MailHeader.CHANGE_NUMBER.fieldWithDelimiter())) {
+ String num = header.substring(MailHeader.CHANGE_NUMBER.fieldWithDelimiter().length());
+ metadata.changeNumber = Ints.tryParse(num);
+ } else if (header.startsWith(MailHeader.PATCH_SET.fieldWithDelimiter())) {
+ String ps = header.substring(MailHeader.PATCH_SET.fieldWithDelimiter().length());
+ metadata.patchSet = Ints.tryParse(ps);
+ } else if (header.startsWith(MailHeader.COMMENT_DATE.fieldWithDelimiter())) {
+ String ts = header.substring(MailHeader.COMMENT_DATE.fieldWithDelimiter().length()).trim();
+ try {
+ metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
+ } catch (DateTimeParseException e) {
+ log.error("Mail: Error while parsing timestamp from header of message " + m.id(), e);
+ }
+ } else if (header.startsWith(MailHeader.MESSAGE_TYPE.fieldWithDelimiter())) {
+ metadata.messageType =
+ header.substring(MailHeader.MESSAGE_TYPE.fieldWithDelimiter().length());
+ }
+ }
+ if (metadata.hasRequiredFields()) {
+ return metadata;
+ }
+
+ // If the required fields were not yet found, continue to parse the text
+ if (!Strings.isNullOrEmpty(m.textContent())) {
+ Iterable<String> lines = Splitter.on('\n').split(m.textContent().replace("\r\n", "\n"));
+ extractFooters(lines, metadata, m);
+ if (metadata.hasRequiredFields()) {
+ return metadata;
+ }
+ }
+
+ // If the required fields were not yet found, continue to parse the HTML
+ // HTML footer are contained inside a <div> tag
+ if (!Strings.isNullOrEmpty(m.htmlContent())) {
+ Iterable<String> lines = Splitter.on("</div>").split(m.htmlContent().replace("\r\n", "\n"));
+ extractFooters(lines, metadata, m);
+ if (metadata.hasRequiredFields()) {
+ return metadata;
+ }
+ }
+
+ return metadata;
+ }
+
+ private static void extractFooters(Iterable<String> lines, MailMetadata metadata, MailMessage m) {
+ for (String line : lines) {
+ if (metadata.changeNumber == null && line.contains(MailHeader.CHANGE_NUMBER.getName())) {
+ metadata.changeNumber =
+ Ints.tryParse(extractFooter(MailHeader.CHANGE_NUMBER.withDelimiter(), line));
+ } else if (metadata.patchSet == null && line.contains(MailHeader.PATCH_SET.getName())) {
+ metadata.patchSet =
+ Ints.tryParse(extractFooter(MailHeader.PATCH_SET.withDelimiter(), line));
+ } else if (metadata.timestamp == null && line.contains(MailHeader.COMMENT_DATE.getName())) {
+ String ts = extractFooter(MailHeader.COMMENT_DATE.withDelimiter(), line);
+ try {
+ metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
+ } catch (DateTimeParseException e) {
+ log.error("Mail: Error while parsing timestamp from footer of message " + m.id(), e);
+ }
+ } else if (metadata.messageType == null && line.contains(MailHeader.MESSAGE_TYPE.getName())) {
+ metadata.messageType = extractFooter(MailHeader.MESSAGE_TYPE.withDelimiter(), line);
+ }
+ }
+ }
+
+ private static String extractFooter(String key, String line) {
+ return line.substring(line.indexOf(key) + key.length(), line.length()).trim();
+ }
+}
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 3f794e8d0..9917261 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -43,6 +43,7 @@
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.mail.MailFilter;
+import com.google.gerrit.server.mail.send.InboundEmailRejectionSender;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.query.change.ChangeData;
@@ -77,6 +78,7 @@
private static final Logger log = LoggerFactory.getLogger(MailProcessor.class);
private final Emails emails;
+ private final InboundEmailRejectionSender.Factory emailRejectionSender;
private final RetryHelper retryHelper;
private final ChangeMessagesUtil changeMessagesUtil;
private final CommentsUtil commentsUtil;
@@ -94,6 +96,7 @@
@Inject
public MailProcessor(
Emails emails,
+ InboundEmailRejectionSender.Factory emailRejectionSender,
RetryHelper retryHelper,
ChangeMessagesUtil changeMessagesUtil,
CommentsUtil commentsUtil,
@@ -108,6 +111,7 @@
AccountCache accountCache,
@CanonicalWebUrl Provider<String> canonicalUrl) {
this.emails = emails;
+ this.emailRejectionSender = emailRejectionSender;
this.retryHelper = retryHelper;
this.changeMessagesUtil = changeMessagesUtil;
this.commentsUtil = commentsUtil;
@@ -148,21 +152,29 @@
}
}
- MailMetadata metadata = MetadataParser.parse(message);
+ MailMetadata metadata = MailHeaderParser.parse(message);
+
if (!metadata.hasRequiredFields()) {
log.error(
String.format(
"Message %s is missing required metadata, have %s. Will delete message.",
message.id(), metadata));
+ sendRejectionEmail(message, InboundEmailRejectionSender.Error.PARSING_ERROR);
return;
}
Set<Account.Id> accountIds = emails.getAccountFor(metadata.author);
+
if (accountIds.size() != 1) {
log.error(
String.format(
"Address %s could not be matched to a unique account. It was matched to %s. Will delete message.",
metadata.author, accountIds));
+
+ // We don't want to send an email if no accounts are linked to it.
+ if (accountIds.size() > 1) {
+ sendRejectionEmail(message, InboundEmailRejectionSender.Error.UNKNOWN_ACCOUNT);
+ }
return;
}
Account.Id accountId = accountIds.iterator().next();
@@ -173,12 +185,23 @@
}
if (!accountState.get().getAccount().isActive()) {
log.warn(String.format("Mail: Account %s is inactive. Will delete message.", accountId));
+ sendRejectionEmail(message, InboundEmailRejectionSender.Error.INACTIVE_ACCOUNT);
return;
}
persistComments(buf, message, metadata, accountId);
}
+ private void sendRejectionEmail(MailMessage message, InboundEmailRejectionSender.Error reason) {
+ try {
+ InboundEmailRejectionSender em =
+ emailRejectionSender.create(message.from(), message.id(), reason);
+ em.send();
+ } catch (Exception e) {
+ log.error("Cannot send email to warn for an error", e);
+ }
+ }
+
private void persistComments(
BatchUpdate.Factory buf, MailMessage message, MailMetadata metadata, Account.Id sender)
throws OrmException, UpdateException, RestApiException {
@@ -190,6 +213,8 @@
String.format(
"Message %s references unique change %s, but there are %d matching changes in the index. Will delete message.",
message.id(), metadata.changeNumber, changeDataList.size()));
+
+ sendRejectionEmail(message, InboundEmailRejectionSender.Error.INTERNAL_EXCEPTION);
return;
}
ChangeData cd = changeDataList.get(0);
@@ -221,6 +246,7 @@
log.warn(
String.format(
"Could not parse any comments from %s. Will delete message.", message.id()));
+ sendRejectionEmail(message, InboundEmailRejectionSender.Error.PARSING_ERROR);
return;
}
diff --git a/java/com/google/gerrit/server/mail/receive/MetadataParser.java b/java/com/google/gerrit/server/mail/receive/MetadataParser.java
deleted file mode 100644
index 88c54f9..0000000
--- a/java/com/google/gerrit/server/mail/receive/MetadataParser.java
+++ /dev/null
@@ -1,110 +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.mail.receive;
-
-import static com.google.gerrit.server.mail.MetadataName.toFooterWithDelimiter;
-import static com.google.gerrit.server.mail.MetadataName.toHeaderWithDelimiter;
-
-import com.google.common.base.Strings;
-import com.google.common.primitives.Ints;
-import com.google.gerrit.server.mail.MailUtil;
-import com.google.gerrit.server.mail.MetadataName;
-import java.sql.Timestamp;
-import java.time.Instant;
-import java.time.format.DateTimeParseException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/** Parse metadata from inbound email */
-public class MetadataParser {
- private static final Logger log = LoggerFactory.getLogger(MetadataParser.class);
-
- public static MailMetadata parse(MailMessage m) {
- MailMetadata metadata = new MailMetadata();
- // Find author
- metadata.author = m.from().getEmail();
-
- // Check email headers for X-Gerrit-<Name>
- for (String header : m.additionalHeaders()) {
- if (header.startsWith(toHeaderWithDelimiter(MetadataName.CHANGE_NUMBER))) {
- String num = header.substring(toHeaderWithDelimiter(MetadataName.CHANGE_NUMBER).length());
- metadata.changeNumber = Ints.tryParse(num);
- } else if (header.startsWith(toHeaderWithDelimiter(MetadataName.PATCH_SET))) {
- String ps = header.substring(toHeaderWithDelimiter(MetadataName.PATCH_SET).length());
- metadata.patchSet = Ints.tryParse(ps);
- } else if (header.startsWith(toHeaderWithDelimiter(MetadataName.TIMESTAMP))) {
- String ts = header.substring(toHeaderWithDelimiter(MetadataName.TIMESTAMP).length()).trim();
- try {
- metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
- } catch (DateTimeParseException e) {
- log.error("Mail: Error while parsing timestamp from header of message " + m.id(), e);
- }
- } else if (header.startsWith(toHeaderWithDelimiter(MetadataName.MESSAGE_TYPE))) {
- metadata.messageType =
- header.substring(toHeaderWithDelimiter(MetadataName.MESSAGE_TYPE).length());
- }
- }
- if (metadata.hasRequiredFields()) {
- return metadata;
- }
-
- // If the required fields were not yet found, continue to parse the text
- if (!Strings.isNullOrEmpty(m.textContent())) {
- String[] lines = m.textContent().replace("\r\n", "\n").split("\n");
- extractFooters(lines, metadata, m);
- if (metadata.hasRequiredFields()) {
- return metadata;
- }
- }
-
- // If the required fields were not yet found, continue to parse the HTML
- // HTML footer are contained inside a <div> tag
- if (!Strings.isNullOrEmpty(m.htmlContent())) {
- String[] lines = m.htmlContent().replace("\r\n", "\n").split("</div>");
- extractFooters(lines, metadata, m);
- if (metadata.hasRequiredFields()) {
- return metadata;
- }
- }
-
- return metadata;
- }
-
- private static void extractFooters(String[] lines, MailMetadata metadata, MailMessage m) {
- for (String line : lines) {
- if (metadata.changeNumber == null && line.contains(MetadataName.CHANGE_NUMBER)) {
- metadata.changeNumber =
- Ints.tryParse(extractFooter(toFooterWithDelimiter(MetadataName.CHANGE_NUMBER), line));
- } else if (metadata.patchSet == null && line.contains(MetadataName.PATCH_SET)) {
- metadata.patchSet =
- Ints.tryParse(extractFooter(toFooterWithDelimiter(MetadataName.PATCH_SET), line));
- } else if (metadata.timestamp == null && line.contains(MetadataName.TIMESTAMP)) {
- String ts = extractFooter(toFooterWithDelimiter(MetadataName.TIMESTAMP), line);
- try {
- metadata.timestamp = Timestamp.from(MailUtil.rfcDateformatter.parse(ts, Instant::from));
- } catch (DateTimeParseException e) {
- log.error("Mail: Error while parsing timestamp from footer of message " + m.id(), e);
- }
- } else if (metadata.messageType == null && line.contains(MetadataName.MESSAGE_TYPE)) {
- metadata.messageType =
- extractFooter(toFooterWithDelimiter(MetadataName.MESSAGE_TYPE), line);
- }
- }
- }
-
- private static String extractFooter(String key, String line) {
- return line.substring(line.indexOf(key) + key.length(), line.length()).trim();
- }
-}
diff --git a/java/com/google/gerrit/server/mail/receive/ParserUtil.java b/java/com/google/gerrit/server/mail/receive/ParserUtil.java
index b8309ab..e770a3e 100644
--- a/java/com/google/gerrit/server/mail/receive/ParserUtil.java
+++ b/java/com/google/gerrit/server/mail/receive/ParserUtil.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.mail.receive;
+import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.gerrit.reviewdb.client.Comment;
import java.util.List;
@@ -37,34 +38,34 @@
*/
public static String trimQuotation(String comment) {
StringJoiner j = new StringJoiner("\n");
- String[] lines = comment.split("\n");
- for (int i = 0; i < lines.length - 2; i++) {
- j.add(lines[i]);
+ List<String> lines = Splitter.on('\n').splitToList(comment);
+ for (int i = 0; i < lines.size() - 2; i++) {
+ j.add(lines.get(i));
}
// Check if the last line contains the full quotation pattern (date + email)
- String lastLine = lines[lines.length - 1];
+ String lastLine = lines.get(lines.size() - 1);
if (containsQuotationPattern(lastLine)) {
- if (lines.length > 1) {
- j.add(lines[lines.length - 2]);
+ if (lines.size() > 1) {
+ j.add(lines.get(lines.size() - 2));
}
return j.toString().trim();
}
// Check if the second last line + the last line contain the full quotation pattern. This is
// necessary, as the quotation line can be split across the last two lines if it gets too long.
- if (lines.length > 1) {
- String lastLines = lines[lines.length - 2] + lastLine;
+ if (lines.size() > 1) {
+ String lastLines = lines.get(lines.size() - 2) + lastLine;
if (containsQuotationPattern(lastLines)) {
return j.toString().trim();
}
}
// Add the last two lines
- if (lines.length > 1) {
- j.add(lines[lines.length - 2]);
+ if (lines.size() > 1) {
+ j.add(lines.get(lines.size() - 2));
}
- j.add(lines[lines.length - 1]);
+ j.add(lines.get(lines.size() - 1));
return j.toString().trim();
}
diff --git a/java/com/google/gerrit/server/mail/receive/TextParser.java b/java/com/google/gerrit/server/mail/receive/TextParser.java
index 80443ba..b99c608 100644
--- a/java/com/google/gerrit/server/mail/receive/TextParser.java
+++ b/java/com/google/gerrit/server/mail/receive/TextParser.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.mail.receive;
+import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.Iterators;
import com.google.common.collect.PeekingIterator;
@@ -63,11 +64,10 @@
PeekingIterator<Comment> iter = Iterators.peekingIterator(comments.iterator());
- String[] lines = body.split("\n");
MailComment currentComment = null;
String lastEncounteredFileName = null;
Comment lastEncounteredComment = null;
- for (String line : lines) {
+ for (String line : Splitter.on('\n').split(body)) {
if (line.equals(">")) {
// Skip empty lines
continue;
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index cad15fa..29fc04f 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -30,6 +30,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
+import com.google.gerrit.server.mail.MailHeader;
import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.PatchList;
@@ -56,6 +57,7 @@
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
+import org.apache.james.mime4j.dom.field.FieldName;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.lib.Repository;
@@ -158,7 +160,7 @@
}
if (patchSet != null) {
- setHeader("X-Gerrit-PatchSet", patchSet.getPatchSetId() + "");
+ setHeader(MailHeader.PATCH_SET.fieldName(), patchSet.getPatchSetId() + "");
if (patchSetInfo == null) {
try {
patchSetInfo =
@@ -178,11 +180,11 @@
super.init();
if (timestamp != null) {
- setHeader("Date", new Date(timestamp.getTime()));
+ setHeader(FieldName.DATE, new Date(timestamp.getTime()));
}
setChangeSubjectHeader();
- setHeader("X-Gerrit-Change-Id", "" + change.getKey().get());
- setHeader("X-Gerrit-Change-Number", "" + change.getChangeId());
+ setHeader(MailHeader.CHANGE_ID.fieldName(), "" + change.getKey().get());
+ setHeader(MailHeader.CHANGE_NUMBER.fieldName(), "" + change.getChangeId());
setChangeUrlHeader();
setCommitIdHeader();
@@ -202,7 +204,7 @@
private void setChangeUrlHeader() {
final String u = getChangeUrl();
if (u != null) {
- setHeader("X-Gerrit-ChangeURL", "<" + u + ">");
+ setHeader(MailHeader.CHANGE_URL.fieldName(), "<" + u + ">");
}
}
@@ -211,12 +213,12 @@
&& patchSet.getRevision() != null
&& patchSet.getRevision().get() != null
&& patchSet.getRevision().get().length() > 0) {
- setHeader("X-Gerrit-Commit", patchSet.getRevision().get());
+ setHeader(MailHeader.COMMIT.fieldName(), patchSet.getRevision().get());
}
}
private void setChangeSubjectHeader() {
- setHeader("Subject", textTemplate("ChangeSubject"));
+ setHeader(FieldName.SUBJECT, textTemplate("ChangeSubject"));
}
/** Get a link to the change; null if the server doesn't know its own address. */
@@ -481,19 +483,18 @@
patchSetInfoData.put("authorEmail", patchSetInfo.getAuthor().getEmail());
soyContext.put("patchSetInfo", patchSetInfoData);
- footers.add("Gerrit-MessageType: " + messageClass);
- footers.add("Gerrit-Change-Id: " + change.getKey().get());
- footers.add("Gerrit-Change-Number: " + Integer.toString(change.getChangeId()));
- footers.add("Gerrit-PatchSet: " + patchSet.getPatchSetId());
- footers.add("Gerrit-Owner: " + getNameEmailFor(change.getOwner()));
+ footers.add(MailHeader.CHANGE_ID.withDelimiter() + change.getKey().get());
+ footers.add(MailHeader.CHANGE_NUMBER.withDelimiter() + Integer.toString(change.getChangeId()));
+ footers.add(MailHeader.PATCH_SET.withDelimiter() + patchSet.getPatchSetId());
+ footers.add(MailHeader.OWNER.withDelimiter() + getNameEmailFor(change.getOwner()));
if (change.getAssignee() != null) {
- footers.add("Gerrit-Assignee: " + getNameEmailFor(change.getAssignee()));
+ footers.add(MailHeader.ASSIGNEE.withDelimiter() + getNameEmailFor(change.getAssignee()));
}
for (String reviewer : getEmailsByState(ReviewerStateInternal.REVIEWER)) {
- footers.add("Gerrit-Reviewer: " + reviewer);
+ footers.add(MailHeader.REVIEWER.withDelimiter() + reviewer);
}
for (String reviewer : getEmailsByState(ReviewerStateInternal.CC)) {
- footers.add("Gerrit-CC: " + reviewer);
+ footers.add(MailHeader.CC.withDelimiter() + reviewer);
}
}
diff --git a/java/com/google/gerrit/server/mail/send/CommentFormatter.java b/java/com/google/gerrit/server/mail/send/CommentFormatter.java
index 74d1480..2590505 100644
--- a/java/com/google/gerrit/server/mail/send/CommentFormatter.java
+++ b/java/com/google/gerrit/server/mail/send/CommentFormatter.java
@@ -16,6 +16,7 @@
import static com.google.common.base.Strings.isNullOrEmpty;
+import com.google.common.base.Splitter;
import com.google.gerrit.common.Nullable;
import java.util.ArrayList;
import java.util.Collections;
@@ -52,7 +53,7 @@
}
List<Block> result = new ArrayList<>();
- for (String p : source.split("\n\n")) {
+ for (String p : Splitter.on("\n\n").split(source)) {
if (isQuote(p)) {
result.add(makeQuote(p));
} else if (isPreFormat(p)) {
@@ -96,7 +97,7 @@
boolean inList = false;
boolean inParagraph = false;
- for (String line : p.split("\n")) {
+ for (String line : Splitter.on('\n').split(p)) {
if (line.startsWith("-") || line.startsWith("*")) {
// The next line looks like a list item. If not building a list already,
// then create one. Remove the list item marker (* or -) from the line.
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 5df0d62..b04dcd6 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -31,6 +31,7 @@
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.mail.MailHeader;
import com.google.gerrit.server.mail.MailUtil;
import com.google.gerrit.server.mail.receive.Protocol;
import com.google.gerrit.server.patch.PatchFile;
@@ -54,6 +55,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import org.apache.james.mime4j.dom.field.FieldName;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
@@ -163,14 +165,14 @@
// Add header that enables identifying comments on parsed email.
// Grouping is currently done by timestamp.
- setHeader("X-Gerrit-Comment-Date", timestamp);
+ setHeader(MailHeader.COMMENT_DATE.fieldName(), timestamp);
if (incomingEmailEnabled) {
if (replyToAddress == null) {
// Remove Reply-To and use outbound SMTP (default) instead.
- removeHeader("Reply-To");
+ removeHeader(FieldName.REPLY_TO);
} else {
- setHeader("Reply-To", replyToAddress);
+ setHeader(FieldName.REPLY_TO, replyToAddress);
}
}
}
@@ -523,12 +525,12 @@
soyContext.put(
"coverLetterBlocks", commentBlocksToSoyData(CommentFormatter.parse(getCoverLetter())));
- footers.add("Gerrit-Comment-Date: " + getCommentTimestamp());
- footers.add("Gerrit-HasComments: " + (hasComments ? "Yes" : "No"));
- footers.add("Gerrit-HasLabels: " + (labels.isEmpty() ? "No" : "Yes"));
+ footers.add(MailHeader.COMMENT_DATE.withDelimiter() + getCommentTimestamp());
+ footers.add(MailHeader.HAS_COMMENTS.withDelimiter() + (hasComments ? "Yes" : "No"));
+ footers.add(MailHeader.HAS_LABELS.withDelimiter() + (labels.isEmpty() ? "No" : "Yes"));
for (Account.Id account : getReplyAccounts()) {
- footers.add("Gerrit-Comment-In-Reply-To: " + getNameEmailFor(account));
+ footers.add(MailHeader.COMMENT_IN_REPLY_TO.withDelimiter() + getNameEmailFor(account));
}
}
diff --git a/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
new file mode 100644
index 0000000..5143dc7
--- /dev/null
+++ b/java/com/google/gerrit/server/mail/send/InboundEmailRejectionSender.java
@@ -0,0 +1,94 @@
+// 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.mail.send;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.MailHeader;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import org.apache.james.mime4j.dom.field.FieldName;
+
+/** Send an email to inform users that parsing their inbound email failed. */
+public class InboundEmailRejectionSender extends OutgoingEmail {
+
+ /** Used by the templating system to determine what error message should be sent */
+ public enum Error {
+ PARSING_ERROR,
+ INACTIVE_ACCOUNT,
+ UNKNOWN_ACCOUNT,
+ INTERNAL_EXCEPTION;
+ }
+
+ public interface Factory {
+ InboundEmailRejectionSender create(Address to, String threadId, Error reason);
+ }
+
+ private final Address to;
+ private final Error reason;
+ private final String threadId;
+
+ @Inject
+ public InboundEmailRejectionSender(
+ EmailArguments ea, @Assisted Address to, @Assisted String threadId, @Assisted Error reason) {
+ super(ea, "error");
+ this.to = checkNotNull(to);
+ this.threadId = checkNotNull(threadId);
+ this.reason = checkNotNull(reason);
+ }
+
+ @Override
+ protected void init() throws EmailException {
+ super.init();
+ setListIdHeader();
+ setHeader(FieldName.SUBJECT, "[Gerrit Code Review] Unable to process your email");
+
+ add(RecipientType.TO, to);
+
+ if (!threadId.isEmpty()) {
+ setHeader(MailHeader.REFERENCES.fieldName(), threadId);
+ }
+ }
+
+ private void setListIdHeader() {
+ // Set a reasonable list id so that filters can be used to sort messages
+ setHeader("List-Id", "<gerrit-noreply." + getGerritHost() + ">");
+ if (getSettingsUrl() != null) {
+ setHeader("List-Unsubscribe", "<" + getSettingsUrl() + ">");
+ }
+ }
+
+ @Override
+ protected void format() throws EmailException {
+ appendText(textTemplate("InboundEmailRejection_" + reason.name()));
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("InboundEmailRejectionHtml_" + reason.name()));
+ }
+ }
+
+ @Override
+ protected void setupSoyContext() {
+ super.setupSoyContext();
+ footers.add(MailHeader.MESSAGE_TYPE.withDelimiter() + messageClass);
+ }
+
+ @Override
+ protected boolean supportsHtml() {
+ return true;
+ }
+}
diff --git a/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java b/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
index b267275..8d7df41 100644
--- a/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
+++ b/java/com/google/gerrit/server/mail/send/MailSoyTofuProvider.java
@@ -51,6 +51,8 @@
"DeleteReviewerHtml.soy",
"DeleteVote.soy",
"DeleteVoteHtml.soy",
+ "InboundEmailRejection.soy",
+ "InboundEmailRejectionHtml.soy",
"Footer.soy",
"FooterHtml.soy",
"HeaderHtml.soy",
@@ -58,6 +60,8 @@
"MergedHtml.soy",
"NewChange.soy",
"NewChangeHtml.soy",
+ "NoReplyFooter.soy",
+ "NoReplyFooterHtml.soy",
"Private.soy",
"RegisterNewEmail.soy",
"ReplacePatchSet.soy",
diff --git a/java/com/google/gerrit/server/mail/send/NotificationEmail.java b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
index 3fefac4..f657fb0 100644
--- a/java/com/google/gerrit/server/mail/send/NotificationEmail.java
+++ b/java/com/google/gerrit/server/mail/send/NotificationEmail.java
@@ -21,6 +21,7 @@
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.MailHeader;
import com.google.gerrit.server.mail.send.ProjectWatch.Watchers;
import com.google.gwtorm.server.OrmException;
import java.util.HashMap;
@@ -115,7 +116,7 @@
branchData.put("shortName", branch.getShortName());
soyContext.put("branch", branchData);
- footers.add("Gerrit-Project: " + branch.getParentKey().get());
+ footers.add(MailHeader.PROJECT.withDelimiter() + branch.getParentKey().get());
footers.add("Gerrit-Branch: " + branch.getShortName());
}
}
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 2d5c13bd..b882089 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -29,6 +29,7 @@
import com.google.gerrit.reviewdb.client.UserIdentity;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.MailHeader;
import com.google.gerrit.server.mail.send.EmailHeader.AddressList;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
@@ -49,6 +50,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
+import org.apache.james.mime4j.dom.field.FieldName;
import org.eclipse.jgit.util.SystemReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -57,9 +59,6 @@
public abstract class OutgoingEmail {
private static final Logger log = LoggerFactory.getLogger(OutgoingEmail.class);
- private static final String HDR_TO = "To";
- private static final String HDR_CC = "CC";
-
protected String messageClass;
private final HashSet<Account.Id> rcptTo = new HashSet<>();
private final Map<String, EmailHeader> headers;
@@ -163,7 +162,7 @@
// Set Reply-To only if it hasn't been set by a child class
// Reply-To will already be populated for the message types where Gerrit supports
// inbound email replies.
- if (!headers.containsKey("Reply-To")) {
+ if (!headers.containsKey(FieldName.REPLY_TO)) {
StringJoiner j = new StringJoiner(", ");
if (fromId != null) {
Address address = toAddress(fromId);
@@ -173,7 +172,7 @@
}
smtpRcptTo.stream().forEach(a -> j.add(a.getEmail()));
smtpRcptToPlaintextOnly.stream().forEach(a -> j.add(a.getEmail()));
- setHeader("Reply-To", j.toString());
+ setHeader(FieldName.REPLY_TO, j.toString());
}
String textPart = textBody.toString();
@@ -208,13 +207,13 @@
Map<String, EmailHeader> shallowCopy = new HashMap<>();
shallowCopy.putAll(headers);
// Remove To and Cc
- shallowCopy.remove(HDR_TO);
- shallowCopy.remove(HDR_CC);
+ shallowCopy.remove(FieldName.TO);
+ shallowCopy.remove(FieldName.CC);
for (Address a : smtpRcptToPlaintextOnly) {
// Add new To
EmailHeader.AddressList to = new EmailHeader.AddressList();
to.add(a);
- shallowCopy.put(HDR_TO, to);
+ shallowCopy.put(FieldName.TO, to);
}
args.emailSender.send(va.smtpFromAddress, smtpRcptToPlaintextOnly, shallowCopy, va.body);
}
@@ -233,17 +232,19 @@
setupSoyContext();
smtpFromAddress = args.fromAddressGenerator.from(fromId);
- setHeader("Date", new Date());
- headers.put("From", new EmailHeader.AddressList(smtpFromAddress));
- headers.put(HDR_TO, new EmailHeader.AddressList());
- headers.put(HDR_CC, new EmailHeader.AddressList());
- setHeader("Message-ID", "");
+ setHeader(FieldName.DATE, new Date());
+ headers.put(FieldName.FROM, new EmailHeader.AddressList(smtpFromAddress));
+ headers.put(FieldName.TO, new EmailHeader.AddressList());
+ headers.put(FieldName.CC, new EmailHeader.AddressList());
+ setHeader(FieldName.MESSAGE_ID, "");
+ setHeader(MailHeader.AUTO_SUBMITTED.fieldName(), "auto-generated");
for (RecipientType recipientType : accountsToNotify.keySet()) {
add(recipientType, accountsToNotify.get(recipientType));
}
- setHeader("X-Gerrit-MessageType", messageClass);
+ setHeader(MailHeader.MESSAGE_TYPE.fieldName(), messageClass);
+ footers.add(MailHeader.MESSAGE_TYPE.withDelimiter() + messageClass);
textBody = new StringBuilder();
htmlBody = new StringBuilder();
@@ -500,15 +501,15 @@
if (!override) {
return;
}
- ((EmailHeader.AddressList) headers.get(HDR_TO)).remove(addr.getEmail());
- ((EmailHeader.AddressList) headers.get(HDR_CC)).remove(addr.getEmail());
+ ((EmailHeader.AddressList) headers.get(FieldName.TO)).remove(addr.getEmail());
+ ((EmailHeader.AddressList) headers.get(FieldName.CC)).remove(addr.getEmail());
}
switch (rt) {
case TO:
- ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
+ ((EmailHeader.AddressList) headers.get(FieldName.TO)).add(addr);
break;
case CC:
- ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
+ ((EmailHeader.AddressList) headers.get(FieldName.CC)).add(addr);
break;
case BCC:
break;
diff --git a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 3e36de9..b9f5fe6 100644
--- a/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -619,7 +619,6 @@
} else {
// OpenRepo buffers objects separately; caller may assume that objects are available in the
// inserter it previously passed via setChangeRepo.
- checkState(saveObjects, "cannot use dryrun with saveObjects = false");
or.flushToFinalInserter();
}
diff --git a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java b/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
index c8d69b9..8e8f232 100644
--- a/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
+++ b/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
@@ -48,9 +48,11 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
+import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfigProvider;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -68,11 +70,13 @@
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.update.ChainedReceiveCommands;
+import com.google.gerrit.server.update.RefUpdateUtil;
import com.google.gerrit.server.util.ManualRequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
@@ -80,10 +84,8 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashSet;
import java.util.List;
import java.util.Optional;
-import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@@ -92,14 +94,17 @@
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.internal.storage.file.PackInserter;
+import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.io.NullOutputStream;
import org.slf4j.Logger;
@@ -131,6 +136,8 @@
public static class Builder {
private final Config cfg;
private final SitePaths sitePaths;
+ private final Provider<PersonIdent> serverIdent;
+ private final AllUsersName allUsers;
private final SchemaFactory<ReviewDb> schemaFactory;
private final GitRepositoryManager repoManager;
private final NoteDbUpdateManager.Factory updateManagerFactory;
@@ -158,6 +165,8 @@
Builder(
GerritServerConfigProvider configProvider,
SitePaths sitePaths,
+ @GerritPersonIdent Provider<PersonIdent> serverIdent,
+ AllUsersName allUsers,
SchemaFactory<ReviewDb> schemaFactory,
GitRepositoryManager repoManager,
NoteDbUpdateManager.Factory updateManagerFactory,
@@ -175,6 +184,8 @@
// trial/autoMigrate get set correctly below.
this.cfg = configProvider.get();
this.sitePaths = sitePaths;
+ this.serverIdent = serverIdent;
+ this.allUsers = allUsers;
this.schemaFactory = schemaFactory;
this.repoManager = repoManager;
this.updateManagerFactory = updateManagerFactory;
@@ -337,6 +348,8 @@
return new NoteDbMigrator(
sitePaths,
schemaFactory,
+ serverIdent,
+ allUsers,
repoManager,
updateManagerFactory,
bundleReader,
@@ -364,6 +377,8 @@
private final FileBasedConfig gerritConfig;
private final FileBasedConfig noteDbConfig;
private final SchemaFactory<ReviewDb> schemaFactory;
+ private final Provider<PersonIdent> serverIdent;
+ private final AllUsersName allUsers;
private final GitRepositoryManager repoManager;
private final NoteDbUpdateManager.Factory updateManagerFactory;
private final ChangeBundleReader bundleReader;
@@ -388,6 +403,8 @@
private NoteDbMigrator(
SitePaths sitePaths,
SchemaFactory<ReviewDb> schemaFactory,
+ Provider<PersonIdent> serverIdent,
+ AllUsersName allUsers,
GitRepositoryManager repoManager,
NoteDbUpdateManager.Factory updateManagerFactory,
ChangeBundleReader bundleReader,
@@ -416,6 +433,8 @@
}
this.schemaFactory = schemaFactory;
+ this.serverIdent = serverIdent;
+ this.allUsers = allUsers;
this.rebuilder = rebuilder;
this.repoManager = repoManager;
this.updateManagerFactory = updateManagerFactory;
@@ -691,10 +710,9 @@
Stopwatch sw = Stopwatch.createStarted();
log.info("Rebuilding changes in NoteDb");
+ ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject = getChangesByProject();
List<ListenableFuture<Boolean>> futures = new ArrayList<>();
try (ContextHelper contextHelper = new ContextHelper()) {
- ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject =
- getChangesByProject(contextHelper.getReviewDb());
List<Project.NameKey> projectNames =
Ordering.usingToString().sortedCopy(changesByProject.keySet());
for (Project.NameKey project : projectNames) {
@@ -723,7 +741,7 @@
}
}
- private ImmutableListMultimap<Project.NameKey, Change.Id> getChangesByProject(ReviewDb db)
+ private ImmutableListMultimap<Project.NameKey, Change.Id> getChangesByProject()
throws OrmException {
// Memoize all changes so we can close the db connection and allow other threads to use the full
// connection pool.
@@ -731,13 +749,15 @@
MultimapBuilder.treeKeys(comparing(Project.NameKey::get))
.treeSetValues(comparing(Change.Id::get))
.build();
- if (!projects.isEmpty()) {
- return byProject(db.changes().all(), c -> projects.contains(c.getProject()), out);
+ try (ReviewDb db = unwrapDb(schemaFactory.open())) {
+ if (!projects.isEmpty()) {
+ return byProject(db.changes().all(), c -> projects.contains(c.getProject()), out);
+ }
+ if (!changes.isEmpty()) {
+ return byProject(db.changes().get(changes), c -> true, out);
+ }
+ return byProject(db.changes().all(), c -> true, out);
}
- if (!changes.isEmpty()) {
- return byProject(db.changes().get(changes), c -> true, out);
- }
- return byProject(db.changes().all(), c -> true, out);
}
private static ImmutableListMultimap<Project.NameKey, Change.Id> byProject(
@@ -767,59 +787,63 @@
new TextProgressMonitor(
new PrintWriter(new BufferedWriter(new OutputStreamWriter(progressOut, UTF_8))));
try (Repository changeRepo = repoManager.openRepository(project);
+ // Only use a PackInserter for the change repo, not All-Users.
+ //
+ // It's not possible to share a single inserter for All-Users across all project tasks, and
+ // we don't want to add one pack per project to All-Users. Adding many loose objects is
+ // preferable to many packs.
+ //
+ // Anyway, the number of objects inserted into All-Users is proportional to the number
+ // of pending draft comments, which should not be high (relative to the total number of
+ // changes), so the number of loose objects shouldn't be too unreasonable.
ObjectInserter changeIns = newPackInserter(changeRepo);
ObjectReader changeReader = changeIns.newReader();
RevWalk changeRw = new RevWalk(changeReader);
- NoteDbUpdateManager manager =
- updateManagerFactory
- .create(project)
- .setSaveObjects(false)
- .setAtomicRefUpdates(false)
- // Only use a PackInserter for the change repo, not All-Users.
- //
- // It's not possible to share a single inserter for All-Users across all project
- // tasks, and we don't want to add one pack per project to All-Users. Adding many
- // loose objects is preferable to many packs.
- //
- // Anyway, the number of objects inserted into All-Users is proportional to the
- // number of pending draft comments, which should not be high (relative to the total
- // number of changes), so the number of loose objects shouldn't be too unreasonable.
- .setChangeRepo(
- changeRepo, changeRw, changeIns, new ChainedReceiveCommands(changeRepo))) {
- Set<Change.Id> skipExecute = new HashSet<>();
+ Repository allUsersRepo = repoManager.openRepository(allUsers);
+ ObjectInserter allUsersIns = allUsersRepo.newObjectInserter();
+ ObjectReader allUsersReader = allUsersIns.newReader();
+ RevWalk allUsersRw = new RevWalk(allUsersReader)) {
+ ChainedReceiveCommands changeCmds = new ChainedReceiveCommands(changeRepo);
+ ChainedReceiveCommands allUsersCmds = new ChainedReceiveCommands(allUsersRepo);
+
Collection<Change.Id> changes = allChanges.get(project);
pm.beginTask(FormatUtil.elide("Rebuilding " + project.get(), 50), changes.size());
+ int toSave = 0;
try {
for (Change.Id changeId : changes) {
- boolean staged = false;
- try {
- stage(db, changeId, manager);
- staged = true;
+ // NoteDbUpdateManager assumes that all commands in its OpenRepo were added by itself, so
+ // we can't share the top-level ChainedReceiveCommands. Use a new set of commands sharing
+ // the same underlying repo, and copy commands back to the top-level
+ // ChainedReceiveCommands later. This also assumes that each ref in the final list of
+ // commands was only modified by a single NoteDbUpdateManager; since we use one manager
+ // per change, and each ref corresponds to exactly one change, this assumption should be
+ // safe.
+ ChainedReceiveCommands tmpChangeCmds =
+ new ChainedReceiveCommands(changeCmds.getRepoRefCache());
+ ChainedReceiveCommands tmpAllUsersCmds =
+ new ChainedReceiveCommands(allUsersCmds.getRepoRefCache());
+
+ try (NoteDbUpdateManager manager =
+ updateManagerFactory
+ .create(project)
+ .setAtomicRefUpdates(false)
+ .setSaveObjects(false)
+ .setChangeRepo(changeRepo, changeRw, changeIns, tmpChangeCmds)
+ .setAllUsersRepo(allUsersRepo, allUsersRw, allUsersIns, tmpAllUsersCmds)) {
+ rebuild(db, changeId, manager);
+
+ // Executing with dryRun=true writes all objects to the underlying inserters and adds
+ // commands to the ChainedReceiveCommands. Afterwards, we can discard the manager, so we
+ // don't keep using any memory beyond what may be buffered in the PackInserter.
+ manager.execute(true);
+
+ tmpChangeCmds.getCommands().values().forEach(c -> addCommand(changeCmds, c));
+ tmpAllUsersCmds.getCommands().values().forEach(c -> addCommand(allUsersCmds, c));
+
+ toSave++;
} catch (NoPatchSetsException e) {
log.warn(e.getMessage());
- } catch (Throwable t) {
- log.error("Failed to rebuild change " + changeId, t);
- ok = false;
- }
- pm.update(1);
- if (!staged) {
- skipExecute.add(changeId);
- }
- }
- } finally {
- pm.endTask();
- }
-
- pm.beginTask(
- FormatUtil.elide("Saving " + project.get(), 50), changes.size() - skipExecute.size());
- try {
- for (Change.Id changeId : changes) {
- if (skipExecute.contains(changeId)) {
- continue;
- }
- try {
- rebuilder.execute(db, changeId, manager, true, false);
- } catch (ConflictingUpdateException e) {
+ } catch (ConflictingUpdateException ex) {
log.warn(
"Rebuilding detected a conflicting ReviewDb update for change {};"
+ " will be auto-rebuilt at runtime",
@@ -834,15 +858,23 @@
pm.endTask();
}
+ pm.beginTask(FormatUtil.elide("Saving " + project.get(), 50), ProgressMonitor.UNKNOWN);
try {
- manager.execute();
+ save(changeRepo, changeRw, changeIns, changeCmds);
+ save(allUsersRepo, allUsersRw, allUsersIns, allUsersCmds);
+ // This isn't really useful progress. If we passed a real ProgressMonitor to
+ // BatchRefUpdate#execute we might get something more incremental, but that doesn't allow us
+ // to specify the repo name in the task text.
+ pm.update(toSave);
} catch (LockFailureException e) {
log.warn(
"Rebuilding detected a conflicting NoteDb update for the following refs, which will"
+ " be auto-rebuilt at runtime: {}",
e.getFailedRefs().stream().distinct().sorted().collect(joining(", ")));
- } catch (OrmException | IOException e) {
+ } catch (IOException e) {
log.error("Failed to save NoteDb state for " + project, e);
+ } finally {
+ pm.endTask();
}
} catch (RepositoryNotFoundException e) {
log.warn("Repository {} not found", project);
@@ -852,7 +884,7 @@
return ok;
}
- private void stage(ReviewDb db, Change.Id changeId, NoteDbUpdateManager manager)
+ private void rebuild(ReviewDb db, Change.Id changeId, NoteDbUpdateManager manager)
throws OrmException, IOException {
// Match ChangeRebuilderImpl#stage, but without calling manager.stage(), since that can only be
// called after building updates for all changes.
@@ -863,6 +895,31 @@
throw new NoSuchChangeException(changeId);
}
rebuilder.buildUpdates(manager, bundleReader.fromReviewDb(db, changeId));
+
+ rebuilder.execute(db, changeId, manager, true, false);
+ }
+
+ private static void addCommand(ChainedReceiveCommands cmds, ReceiveCommand cmd) {
+ // ChainedReceiveCommands doesn't allow no-ops, but these occur when rebuilding a
+ // previously-rebuilt change.
+ if (!cmd.getOldId().equals(cmd.getNewId())) {
+ cmds.add(cmd);
+ }
+ }
+
+ private void save(Repository repo, RevWalk rw, ObjectInserter ins, ChainedReceiveCommands cmds)
+ throws IOException {
+ if (cmds.isEmpty()) {
+ return;
+ }
+ ins.flush();
+ BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate();
+ bru.setRefLogMessage("Migrate changes to NoteDb", false);
+ bru.setRefLogIdent(serverIdent.get());
+ bru.setAtomic(false);
+ bru.setAllowNonFastForwards(true);
+ cmds.addTo(bru);
+ RefUpdateUtil.executeChecked(bru, rw);
}
private static boolean futuresToBoolean(List<ListenableFuture<Boolean>> futures, String errMsg) {
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index 01d5fa3..800d877 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -37,6 +37,7 @@
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
@Singleton
@@ -159,18 +160,21 @@
}
private Boolean computeAdmin() {
- Boolean r = user.get(IS_ADMIN);
- if (r == null) {
- if (user.isImpersonating()) {
- r = false;
- } else if (user instanceof PeerDaemonUser) {
- r = true;
- } else {
- r = allow(capabilities().administrateServer);
- }
- user.put(IS_ADMIN, r);
+ Optional<Boolean> r = user.get(IS_ADMIN);
+ if (r.isPresent()) {
+ return r.get();
}
- return r;
+
+ boolean isAdmin;
+ if (user.isImpersonating()) {
+ isAdmin = false;
+ } else if (user instanceof PeerDaemonUser) {
+ isAdmin = true;
+ } else {
+ isAdmin = allow(capabilities().administrateServer);
+ }
+ user.put(IS_ADMIN, isAdmin);
+ return isAdmin;
}
private boolean canEmailReviewers() {
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackendModule.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackendModule.java
index d9a43a9..f3a3c78 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackendModule.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackendModule.java
@@ -28,8 +28,9 @@
public static class LegacyControlsModule extends FactoryModule {
@Override
protected void configure() {
- // TODO(sop) Hide ProjectControl, RefControl, ChangeControl related bindings.
+ // TODO(hiesel) Hide ProjectControl, RefControl, ChangeControl related bindings.
factory(ProjectControl.Factory.class);
+ factory(DefaultRefFilter.Factory.class);
bind(ChangeControl.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/git/VisibleRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
similarity index 74%
rename from java/com/google/gerrit/server/git/VisibleRefFilter.java
rename to java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 3efca3f..28695b8 100644
--- a/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.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;
+package com.google.gerrit.server.permissions;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CACHE_AUTOMERGE;
@@ -21,6 +21,7 @@
import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS_SELF;
import static java.util.stream.Collectors.toMap;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Account;
@@ -33,15 +34,13 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.git.SearchingChangeCacheImpl;
+import com.google.gerrit.server.git.TagCache;
+import com.google.gerrit.server.git.TagMatcher;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeNotes.Factory.ChangeNotesResult;
-import com.google.gerrit.server.permissions.ChangePermission;
-import com.google.gerrit.server.permissions.GlobalPermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.ProjectPermission;
-import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
@@ -58,67 +57,56 @@
import java.util.stream.Stream;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.SymbolicRef;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.AbstractAdvertiseRefsHook;
-import org.eclipse.jgit.transport.ServiceMayNotContinueException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
- private static final Logger log = LoggerFactory.getLogger(VisibleRefFilter.class);
+class DefaultRefFilter {
+ private static final Logger log = LoggerFactory.getLogger(DefaultRefFilter.class);
- public interface Factory {
- VisibleRefFilter create(ProjectState projectState, Repository git);
+ interface Factory {
+ DefaultRefFilter create(ProjectControl projectControl);
}
private final TagCache tagCache;
private final ChangeNotes.Factory changeNotesFactory;
@Nullable private final SearchingChangeCacheImpl changeCache;
private final Provider<ReviewDb> db;
- private final Provider<CurrentUser> user;
private final GroupCache groupCache;
private final PermissionBackend permissionBackend;
- private final PermissionBackend.ForProject perm;
+ private final ProjectControl projectControl;
+ private final CurrentUser user;
private final ProjectState projectState;
- private final Repository git;
- private boolean showMetadata = true;
- private String userEditPrefix;
+ private final PermissionBackend.ForProject permissionBackendForProject;
+
private Map<Change.Id, Branch.NameKey> visibleChanges;
@Inject
- VisibleRefFilter(
+ DefaultRefFilter(
TagCache tagCache,
ChangeNotes.Factory changeNotesFactory,
@Nullable SearchingChangeCacheImpl changeCache,
Provider<ReviewDb> db,
- Provider<CurrentUser> user,
GroupCache groupCache,
PermissionBackend permissionBackend,
- @Assisted ProjectState projectState,
- @Assisted Repository git) {
+ @Assisted ProjectControl projectControl) {
this.tagCache = tagCache;
this.changeNotesFactory = changeNotesFactory;
this.changeCache = changeCache;
this.db = db;
- this.user = user;
this.groupCache = groupCache;
this.permissionBackend = permissionBackend;
- this.perm =
- permissionBackend.user(user).database(db).project(projectState.getProject().getNameKey());
- this.projectState = projectState;
- this.git = git;
+ this.projectControl = projectControl;
+
+ this.user = projectControl.getUser();
+ this.projectState = projectControl.getProjectState();
+ this.permissionBackendForProject =
+ permissionBackend.user(user).database(db).project(projectState.getNameKey());
}
- /** Show change references. Default is {@code true}. */
- public VisibleRefFilter setShowMetadata(boolean show) {
- showMetadata = show;
- return this;
- }
-
- public Map<String, Ref> filter(Map<String, Ref> refs, boolean filterTagsSeparately) {
+ Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
+ throws PermissionBackendException {
if (projectState.isAllUsers()) {
refs = addUsersSelfSymref(refs);
}
@@ -128,7 +116,7 @@
if (!projectState.isAllUsers()) {
if (checkProjectPermission(forProject, ProjectPermission.READ)) {
return refs;
- } else if (checkProjectPermission(forProject, ProjectPermission.READ_NO_CONFIG)) {
+ } else if (projectControl.allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG))) {
return fastHideRefsMetaConfig(refs);
}
}
@@ -137,12 +125,11 @@
boolean isAdmin;
Account.Id userId;
IdentifiedUser identifiedUser;
- if (user.get().isIdentifiedUser()) {
+ if (user.isIdentifiedUser()) {
viewMetadata = withUser.testOrFalse(GlobalPermission.ACCESS_DATABASE);
isAdmin = withUser.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER);
- identifiedUser = user.get().asIdentifiedUser();
+ identifiedUser = user.asIdentifiedUser();
userId = identifiedUser.getAccountId();
- userEditPrefix = RefNames.refsEditPrefix(userId);
} else {
viewMetadata = false;
isAdmin = false;
@@ -158,16 +145,16 @@
Change.Id changeId;
Account.Id accountId;
AccountGroup.UUID accountGroupUuid;
- if (name.startsWith(REFS_CACHE_AUTOMERGE) || (!showMetadata && isMetadata(name))) {
+ if (name.startsWith(REFS_CACHE_AUTOMERGE) || (opts.filterMeta() && isMetadata(name))) {
continue;
} else if (RefNames.isRefsEdit(name)) {
// Edits are visible only to the owning user, if change is visible.
- if (viewMetadata || visibleEdit(name)) {
+ if (viewMetadata || visibleEdit(repo, name)) {
result.put(name, ref);
}
} else if ((changeId = Change.Id.fromRef(name)) != null) {
// Change ref is visible only if the change is visible.
- if (viewMetadata || visible(changeId)) {
+ if (viewMetadata || visible(repo, changeId)) {
result.put(name, ref);
}
} else if ((accountId = Account.Id.fromRef(name)) != null) {
@@ -218,14 +205,20 @@
// If we have tags that were deferred, we need to do a revision walk
// to identify what tags we can actually reach, and what we cannot.
//
- if (!deferredTags.isEmpty() && (!result.isEmpty() || filterTagsSeparately)) {
+ if (!deferredTags.isEmpty() && (!result.isEmpty() || opts.filterTagsSeparately())) {
TagMatcher tags =
tagCache
.get(projectState.getNameKey())
.matcher(
tagCache,
- git,
- filterTagsSeparately ? filter(git.getAllRefs()).values() : result.values());
+ repo,
+ opts.filterTagsSeparately()
+ ? filter(
+ repo.getAllRefs(),
+ repo,
+ opts.toBuilder().setFilterTagsSeparately(false).build())
+ .values()
+ : result.values());
for (Ref tag : deferredTags) {
if (tags.isReachable(tag)) {
result.put(tag.getName(), tag);
@@ -246,8 +239,8 @@
}
private Map<String, Ref> addUsersSelfSymref(Map<String, Ref> refs) {
- if (user.get().isIdentifiedUser()) {
- Ref r = refs.get(RefNames.refsUsers(user.get().getAccountId()));
+ if (user.isIdentifiedUser()) {
+ Ref r = refs.get(RefNames.refsUsers(user.getAccountId()));
if (r != null) {
SymbolicRef s = new SymbolicRef(REFS_USERS_SELF, r);
refs = new HashMap<>(refs);
@@ -257,28 +250,10 @@
return refs;
}
- @Override
- protected Map<String, Ref> getAdvertisedRefs(Repository repository, RevWalk revWalk)
- throws ServiceMayNotContinueException {
- try {
- return filter(repository.getRefDatabase().getRefs(RefDatabase.ALL));
- } catch (ServiceMayNotContinueException e) {
- throw e;
- } catch (IOException e) {
- ServiceMayNotContinueException ex = new ServiceMayNotContinueException();
- ex.initCause(e);
- throw ex;
- }
- }
-
- private Map<String, Ref> filter(Map<String, Ref> refs) {
- return filter(refs, false);
- }
-
- private boolean visible(Change.Id changeId) {
+ private boolean visible(Repository repo, Change.Id changeId) {
if (visibleChanges == null) {
if (changeCache == null) {
- visibleChanges = visibleChangesByScan();
+ visibleChanges = visibleChangesByScan(repo);
} else {
visibleChanges = visibleChangesBySearch();
}
@@ -286,22 +261,26 @@
return visibleChanges.containsKey(changeId);
}
- private boolean visibleEdit(String name) {
+ private boolean visibleEdit(Repository repo, String name) {
Change.Id id = Change.Id.fromEditRefPart(name);
// Initialize if it wasn't yet
if (visibleChanges == null) {
- visible(id);
+ visible(repo, id);
}
if (id == null) {
return false;
}
- if (userEditPrefix != null && name.startsWith(userEditPrefix) && visible(id)) {
+ if (user.isIdentifiedUser()
+ && name.startsWith(RefNames.refsEditPrefix(user.asIdentifiedUser().getAccountId()))
+ && visible(repo, id)) {
return true;
}
if (visibleChanges.containsKey(id)) {
try {
// Default to READ_PRIVATE_CHANGES as there is no special permission for reading edits.
- perm.ref(visibleChanges.get(id).get()).check(RefPermission.READ_PRIVATE_CHANGES);
+ permissionBackendForProject
+ .ref(visibleChanges.get(id).get())
+ .check(RefPermission.READ_PRIVATE_CHANGES);
return true;
} catch (PermissionBackendException | AuthException e) {
return false;
@@ -317,7 +296,7 @@
for (ChangeData cd : changeCache.getChangeData(db.get(), project)) {
ChangeNotes notes = changeNotesFactory.createFromIndexedChange(cd.change());
if (projectState.statePermitsRead()
- && perm.indexedChange(cd, notes).test(ChangePermission.READ)) {
+ && permissionBackendForProject.indexedChange(cd, notes).test(ChangePermission.READ)) {
visibleChanges.put(cd.getId(), cd.change().getDest());
}
}
@@ -329,32 +308,34 @@
}
}
- private Map<Change.Id, Branch.NameKey> visibleChangesByScan() {
+ private Map<Change.Id, Branch.NameKey> visibleChangesByScan(Repository repo) {
Project.NameKey p = projectState.getNameKey();
Stream<ChangeNotesResult> s;
try {
- s = changeNotesFactory.scan(git, db.get(), p);
+ s = changeNotesFactory.scan(repo, db.get(), p);
} catch (IOException e) {
log.error("Cannot load changes for project " + p + ", assuming no changes are visible", e);
return Collections.emptyMap();
}
- return s.map(r -> toNotes(p, r))
+ return s.map(r -> toNotes(r))
.filter(Objects::nonNull)
.collect(toMap(n -> n.getChangeId(), n -> n.getChange().getDest()));
}
@Nullable
- private ChangeNotes toNotes(Project.NameKey p, ChangeNotesResult r) {
+ private ChangeNotes toNotes(ChangeNotesResult r) {
if (r.error().isPresent()) {
- log.warn("Failed to load change " + r.id() + " in " + p, r.error().get());
+ log.warn(
+ "Failed to load change " + r.id() + " in " + projectState.getName(), r.error().get());
return null;
}
try {
- if (projectState.statePermitsRead() && perm.change(r.notes()).test(ChangePermission.READ)) {
+ if (projectState.statePermitsRead()
+ && permissionBackendForProject.change(r.notes()).test(ChangePermission.READ)) {
return r.notes();
}
} catch (PermissionBackendException e) {
- log.warn("Failed to check permission for " + r.id() + " in " + p, e);
+ log.warn("Failed to check permission for " + r.id() + " in " + projectState.getName(), e);
}
return null;
}
@@ -373,7 +354,7 @@
private boolean canReadRef(String ref) {
try {
- perm.ref(ref).check(RefPermission.READ);
+ permissionBackendForProject.ref(ref).check(RefPermission.READ);
} catch (AuthException e) {
return false;
} catch (PermissionBackendException e) {
@@ -392,8 +373,7 @@
} catch (PermissionBackendException e) {
log.error(
String.format(
- "Can't check permission for user %s on project %s",
- user.get(), projectState.getName()),
+ "Can't check permission for user %s on project %s", user, projectState.getName()),
e);
return false;
}
diff --git a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index 5144833..209de69 100644
--- a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -20,10 +20,14 @@
import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
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.query.change.ChangeData;
import com.google.inject.Provider;
import java.util.Collection;
+import java.util.Map;
import java.util.Set;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
/**
* Helpers for {@link PermissionBackend} that must fail.
@@ -103,6 +107,12 @@
throws PermissionBackendException {
throw new PermissionBackendException(message, cause);
}
+
+ @Override
+ public Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
+ throws PermissionBackendException {
+ throw new PermissionBackendException(message, cause);
+ }
}
private static class FailedRef extends ForRef {
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 0c4f228..56c300d 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.stream.Collectors.toSet;
+import com.google.auto.value.AutoValue;
import com.google.common.collect.Sets;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission;
@@ -36,7 +37,10 @@
import java.util.Collections;
import java.util.EnumSet;
import java.util.Iterator;
+import java.util.Map;
import java.util.Set;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -305,6 +309,45 @@
public BooleanCondition testCond(ProjectPermission perm) {
return new PermissionBackendCondition.ForProject(this, perm);
}
+
+ /**
+ * @return a partition of the provided refs that are visible to the user that this instance is
+ * scoped to.
+ */
+ public abstract Map<String, Ref> filter(
+ Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
+ throws PermissionBackendException;
+ }
+
+ /** Options for filtering refs using {@link ForProject}. */
+ @AutoValue
+ public abstract static class RefFilterOptions {
+ /** Remove all NoteDb refs (refs/changes/*, refs/users/*, edit refs) from the result. */
+ public abstract boolean filterMeta();
+
+ /** Separately add reachable tags. */
+ public abstract boolean filterTagsSeparately();
+
+ public abstract Builder toBuilder();
+
+ public static Builder builder() {
+ return new AutoValue_PermissionBackend_RefFilterOptions.Builder()
+ .setFilterMeta(false)
+ .setFilterTagsSeparately(false);
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setFilterMeta(boolean val);
+
+ public abstract Builder setFilterTagsSeparately(boolean val);
+
+ public abstract RefFilterOptions build();
+ }
+
+ public static RefFilterOptions defaults() {
+ return builder().build();
+ }
}
/** PermissionBackend scoped to a user, project and reference. */
diff --git a/java/com/google/gerrit/server/permissions/PermissionCollection.java b/java/com/google/gerrit/server/permissions/PermissionCollection.java
index f3da9c5..e8e6030 100644
--- a/java/com/google/gerrit/server/permissions/PermissionCollection.java
+++ b/java/com/google/gerrit/server/permissions/PermissionCollection.java
@@ -14,18 +14,18 @@
package com.google.gerrit.server.permissions;
-import static com.google.common.base.MoreObjects.firstNonNull;
+import static com.google.gerrit.common.data.PermissionRule.Action.BLOCK;
import static com.google.gerrit.server.project.RefPattern.isRE;
+import static java.util.stream.Collectors.mapping;
+import static java.util.stream.Collectors.toList;
import com.google.auto.value.AutoValue;
-import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.MultimapBuilder;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
@@ -35,13 +35,13 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
/**
* Effective permissions applied to a reference in a project.
@@ -126,78 +126,134 @@
// LinkedHashMap to maintain input ordering.
Map<AccessSection, Project.NameKey> sectionToProject = new LinkedHashMap<>();
boolean perUser = filterRefMatchingSections(matcherList, ref, user, sectionToProject);
-
List<AccessSection> sections = Lists.newArrayList(sectionToProject.keySet());
// Sort by ref pattern specificity. For equally specific patterns, the sections from the
// project closer to the current one come first.
sorter.sort(ref, sections);
- Set<SeenRule> seen = new HashSet<>();
- Set<String> exclusiveGroupPermissions = new HashSet<>();
+ // 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()));
- HashMap<String, List<PermissionRule>> permissions = new HashMap<>();
- HashMap<String, List<PermissionRule>> overridden = new HashMap<>();
- Map<PermissionRule, ProjectRef> ruleProps = Maps.newIdentityHashMap();
- ListMultimap<Project.NameKey, String> exclusivePermissionsByProject =
- MultimapBuilder.hashKeys().arrayListValues().build();
- for (AccessSection section : sections) {
- Project.NameKey project = sectionToProject.get(section);
- for (Permission permission : section.getPermissions()) {
- boolean exclusivePermissionExists =
- exclusiveGroupPermissions.contains(permission.getName());
-
- for (PermissionRule rule : permission.getRules()) {
- SeenRule s = SeenRule.create(section, permission, rule);
- boolean addRule;
- if (rule.isBlock()) {
- addRule = !exclusivePermissionsByProject.containsEntry(project, permission.getName());
- } else {
- addRule = seen.add(s) && !rule.isDeny() && !exclusivePermissionExists;
- }
-
- HashMap<String, List<PermissionRule>> p = null;
- if (addRule) {
- p = permissions;
- } else if (!rule.isDeny() && !exclusivePermissionExists) {
- p = overridden;
- }
-
- if (p != null) {
- List<PermissionRule> r = p.get(permission.getName());
- if (r == null) {
- r = new ArrayList<>(2);
- p.put(permission.getName(), r);
- }
- r.add(rule);
- ruleProps.put(rule, ProjectRef.create(project, section.getName()));
- }
- }
-
- if (permission.getExclusiveGroup()) {
- exclusivePermissionsByProject.put(project, permission.getName());
- exclusiveGroupPermissions.add(permission.getName());
- }
- }
+ Map<Project.NameKey, List<AccessSection>> accessByProject =
+ accessDescending
+ .stream()
+ .collect(
+ Collectors.groupingBy(
+ e -> e.getValue(), LinkedHashMap::new, mapping(e -> e.getKey(), toList())));
+ // Within each project, sort by ref specificity.
+ for (List<AccessSection> secs : accessByProject.values()) {
+ sorter.sort(ref, secs);
}
- return new PermissionCollection(permissions, overridden, ruleProps, perUser);
+ return new PermissionCollection(
+ Lists.newArrayList(accessByProject.values()), sections, perUser);
}
}
- private final Map<String, List<PermissionRule>> rules;
- private final Map<String, List<PermissionRule>> overridden;
- private final Map<PermissionRule, ProjectRef> ruleProps;
+ /** Returns permissions in the right order for evaluating BLOCK status. */
+ List<List<Permission>> getBlockRules(String perm) {
+ List<List<Permission>> ps = blockPerProjectByPermission.get(perm);
+ if (ps == null) {
+ ps = calculateBlockRules(perm);
+ blockPerProjectByPermission.put(perm, ps);
+ }
+ return ps;
+ }
+
+ /** Returns permissions in the right order for evaluating ALLOW/DENY status. */
+ List<PermissionRule> getAllowRules(String perm) {
+ List<PermissionRule> ps = rulesByPermission.get(perm);
+ if (ps == null) {
+ ps = calculateAllowRules(perm);
+ rulesByPermission.put(perm, ps);
+ }
+ return ps;
+ }
+
+ /** calculates permissions for ALLOW processing. */
+ private List<PermissionRule> calculateAllowRules(String permName) {
+ Set<SeenRule> seen = new HashSet<>();
+
+ List<PermissionRule> r = new ArrayList<>();
+ for (AccessSection s : accessSectionsUpward) {
+ Permission p = s.getPermission(permName);
+ if (p == null) {
+ continue;
+ }
+ for (PermissionRule pr : p.getRules()) {
+ SeenRule sr = SeenRule.create(s, pr);
+ if (seen.contains(sr)) {
+ // We allow only one rule per (ref-pattern, group) tuple. This is used to implement DENY:
+ // If we see a DENY before an ALLOW rule, that causes the ALLOW rule to be skipped here,
+ // negating access.
+ continue;
+ }
+ seen.add(sr);
+
+ if (pr.getAction() == BLOCK) {
+ // Block rules are handled elsewhere.
+ continue;
+ }
+
+ if (pr.getAction() == PermissionRule.Action.DENY) {
+ // DENY rules work by not adding ALLOW rules. Nothing else to do.
+ continue;
+ }
+ r.add(pr);
+ }
+ if (p.getExclusiveGroup()) {
+ // We found an exclusive permission, so no need to further go up the hierarchy.
+ break;
+ }
+ }
+ return r;
+ }
+
+ // Calculates the inputs for determining BLOCK status, grouped by project.
+ private List<List<Permission>> calculateBlockRules(String permName) {
+ List<List<Permission>> result = new ArrayList<>();
+ for (List<AccessSection> secs : this.accessSectionsPerProjectDownward) {
+ List<Permission> perms = new ArrayList<>();
+ boolean blockFound = false;
+ for (AccessSection sec : secs) {
+ Permission p = sec.getPermission(permName);
+ if (p == null) {
+ continue;
+ }
+ for (PermissionRule pr : p.getRules()) {
+ if (blockFound || pr.getAction() == Action.BLOCK) {
+ blockFound = true;
+ break;
+ }
+ }
+
+ perms.add(p);
+ }
+
+ if (blockFound) {
+ result.add(perms);
+ }
+ }
+ return result;
+ }
+
+ private List<List<AccessSection>> accessSectionsPerProjectDownward;
+ private List<AccessSection> accessSectionsUpward;
+
+ private final Map<String, List<PermissionRule>> rulesByPermission;
+ private final Map<String, List<List<Permission>>> blockPerProjectByPermission;
private final boolean perUser;
private PermissionCollection(
- Map<String, List<PermissionRule>> rules,
- Map<String, List<PermissionRule>> overridden,
- Map<PermissionRule, ProjectRef> ruleProps,
+ List<List<AccessSection>> accessSectionsDownward,
+ List<AccessSection> accessSectionsUpward,
boolean perUser) {
- this.rules = rules;
- this.overridden = overridden;
- this.ruleProps = ruleProps;
+ this.accessSectionsPerProjectDownward = accessSectionsDownward;
+ this.accessSectionsUpward = accessSectionsUpward;
+ this.rulesByPermission = new HashMap<>();
+ this.blockPerProjectByPermission = new HashMap<>();
this.perUser = perUser;
}
@@ -209,55 +265,18 @@
return perUser;
}
- /**
- * Obtain all permission rules for a given type of permission.
- *
- * @param permissionName type of permission.
- * @return all rules that apply to this reference, for any group. Never null; the empty list is
- * returned when there are no rules for the requested permission name.
- */
- public List<PermissionRule> getPermission(String permissionName) {
- List<PermissionRule> r = rules.get(permissionName);
- return r != null ? r : Collections.<PermissionRule>emptyList();
- }
-
- List<PermissionRule> getOverridden(String permissionName) {
- return firstNonNull(overridden.get(permissionName), Collections.<PermissionRule>emptyList());
- }
-
- ProjectRef getRuleProps(PermissionRule rule) {
- return ruleProps.get(rule);
- }
-
- /**
- * Obtain all declared permission rules that match the reference.
- *
- * @return all rules. The collection will iterate a permission if it was declared in the project
- * configuration, either directly or inherited. If the project owner did not use a known
- * permission (for example {@link Permission#FORGE_SERVER}, then it will not be represented in
- * the result even if {@link #getPermission(String)} returns an empty list for the same
- * permission.
- */
- public Iterable<Map.Entry<String, List<PermissionRule>>> getDeclaredPermissions() {
- return rules.entrySet();
- }
-
/** (ref, permission, group) tuple. */
@AutoValue
abstract static class SeenRule {
public abstract String refPattern();
- public abstract String permissionName();
-
@Nullable
public abstract AccountGroup.UUID group();
- static SeenRule create(
- AccessSection section, Permission permission, @Nullable PermissionRule rule) {
+ static SeenRule create(AccessSection section, @Nullable PermissionRule rule) {
AccountGroup.UUID group =
rule != null && rule.getGroup() != null ? rule.getGroup().getUUID() : null;
- return new AutoValue_PermissionCollection_SeenRule(
- section.getName(), permission.getName(), group);
+ return new AutoValue_PermissionCollection_SeenRule(section.getName(), group);
}
}
}
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index 7441086..00c3948 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -16,7 +16,6 @@
import static com.google.common.base.Preconditions.checkArgument;
-import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
@@ -36,6 +35,7 @@
import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
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.project.ProjectState;
import com.google.gerrit.server.project.SectionMatcher;
import com.google.gerrit.server.query.change.ChangeData;
@@ -50,6 +50,8 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
/** Access control management for a user accessing a project's data. */
class ProjectControl {
@@ -64,6 +66,7 @@
private final ProjectState state;
private final ChangeControl.Factory changeControlFactory;
private final PermissionCollection.Factory permissionFilter;
+ private final DefaultRefFilter.Factory refFilterFactory;
private List<SectionMatcher> allSections;
private Map<String, RefControl> refControls;
@@ -76,6 +79,7 @@
PermissionCollection.Factory permissionFilter,
ChangeControl.Factory changeControlFactory,
PermissionBackend permissionBackend,
+ DefaultRefFilter.Factory refFilterFactory,
@Assisted CurrentUser who,
@Assisted ProjectState ps) {
this.changeControlFactory = changeControlFactory;
@@ -83,6 +87,7 @@
this.receiveGroups = receiveGroups;
this.permissionFilter = permissionFilter;
this.permissionBackend = permissionBackend;
+ this.refFilterFactory = refFilterFactory;
user = who;
state = ps;
}
@@ -95,6 +100,7 @@
permissionFilter,
changeControlFactory,
permissionBackend,
+ refFilterFactory,
who,
state);
// Not per-user, and reusing saves lookup time.
@@ -172,6 +178,10 @@
return match(rule.getGroup().getUUID(), isChangeOwner);
}
+ boolean allRefsAreVisible(Set<String> ignore) {
+ return user.isInternalUser() || canPerformOnAllRefs(Permission.READ, ignore);
+ }
+
/** Can the user run upload pack? */
private boolean canRunUploadPack() {
for (AccountGroup.UUID group : uploadGroups) {
@@ -192,10 +202,6 @@
return false;
}
- private boolean allRefsAreVisible(Set<String> ignore) {
- return user.isInternalUser() || canPerformOnAllRefs(Permission.READ, ignore);
- }
-
/** Returns whether the project is hidden. */
private boolean isHidden() {
return getProject().getState().equals(com.google.gerrit.extensions.client.ProjectState.HIDDEN);
@@ -312,6 +318,7 @@
}
private class ForProjectImpl extends ForProject {
+ private DefaultRefFilter refFilter;
private String resourcePath;
@Override
@@ -381,6 +388,15 @@
return ok;
}
+ @Override
+ public Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
+ throws PermissionBackendException {
+ if (refFilter == null) {
+ refFilter = refFilterFactory.create(ProjectControl.this);
+ }
+ return refFilter.filter(refs, repo, opts);
+ }
+
private boolean can(ProjectPermission perm) throws PermissionBackendException {
switch (perm) {
case ACCESS:
@@ -390,9 +406,6 @@
case READ:
return !isHidden() && allRefsAreVisible(Collections.emptySet());
- case READ_NO_CONFIG:
- return !isHidden() && allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG));
-
case CREATE_REF:
return canAddRefs();
case CREATE_CHANGE:
@@ -406,9 +419,11 @@
case PUSH_AT_LEAST_ONE_REF:
return canPushToAtLeastOneRef();
+ case READ_CONFIG:
+ return controlForRef(RefNames.REFS_CONFIG).isVisible();
+
case BAN_COMMIT:
case READ_REFLOG:
- case READ_CONFIG:
case WRITE_CONFIG:
return isOwner();
}
diff --git a/java/com/google/gerrit/server/permissions/ProjectPermission.java b/java/com/google/gerrit/server/permissions/ProjectPermission.java
index 6627f76..37f3726 100644
--- a/java/com/google/gerrit/server/permissions/ProjectPermission.java
+++ b/java/com/google/gerrit/server/permissions/ProjectPermission.java
@@ -35,14 +35,6 @@
READ(Permission.READ),
/**
- * Can read all non-config references in the repository.
- *
- * <p>This is the same as {@code READ} but does not check if they user can see refs/meta/config.
- * Therefore, callers should check {@code READ} before excluding config refs in a short-circuit.
- */
- READ_NO_CONFIG,
-
- /**
* Can create at least one reference in the project.
*
* <p>This project level permission only validates the user may create some type of reference
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 0cae939..45ef4c7 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -19,6 +19,7 @@
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.BooleanProjectConfig;
import com.google.gerrit.reviewdb.client.Change;
@@ -32,14 +33,9 @@
import com.google.gerrit.server.util.MagicBranch;
import com.google.gwtorm.server.OrmException;
import com.google.inject.util.Providers;
-import java.util.ArrayList;
import java.util.Collection;
-import java.util.Collections;
import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
/** Manages access control for Git references (aka branches, tags). */
@@ -50,8 +46,7 @@
/** All permissions that apply to this reference. */
private final PermissionCollection relevant;
- /** Cached set of permissions matching this user. */
- private final Map<String, List<PermissionRule>> effective;
+ // The next 4 members are cached canPerform() permissions.
private Boolean owner;
private Boolean canForgeAuthor;
@@ -62,7 +57,6 @@
this.projectControl = projectControl;
this.refName = ref;
this.relevant = relevant;
- this.effective = new HashMap<>();
}
ProjectControl getProjectControl() {
@@ -124,12 +118,12 @@
// granting of powers beyond submitting to the configuration.
return projectControl.isOwner();
}
- return canPerform(Permission.SUBMIT, isChangeOwner);
+ return canPerform(Permission.SUBMIT, isChangeOwner, false);
}
/** @return true if this user can force edit topic names. */
boolean canForceEditTopicName() {
- return canForcePerform(Permission.EDIT_TOPIC_NAME);
+ return canPerform(Permission.EDIT_TOPIC_NAME, false, true);
}
/** The range of permitted values associated with a label permission. */
@@ -140,14 +134,14 @@
/** The range of permitted values associated with a label permission. */
PermissionRange getRange(String permission, boolean isChangeOwner) {
if (Permission.hasRange(permission)) {
- return toRange(permission, access(permission, isChangeOwner));
+ return toRange(permission, isChangeOwner);
}
return null;
}
/** True if the user has this permission. Works only for non labels. */
boolean canPerform(String permissionName) {
- return canPerform(permissionName, false);
+ return canPerform(permissionName, false, false);
}
ForRef asForRef() {
@@ -198,7 +192,7 @@
case UNKNOWN:
case WEB_BROWSER:
default:
- return (isOwner() && !isForceBlocked(Permission.PUSH)) || projectControl.isAdmin();
+ return (isOwner() && !isBlocked(Permission.PUSH, false, true)) || projectControl.isAdmin();
}
}
@@ -211,7 +205,7 @@
// granting of powers beyond pushing to the configuration.
return false;
}
- return canForcePerform(Permission.PUSH);
+ return canPerform(Permission.PUSH, false, true);
}
/**
@@ -239,7 +233,10 @@
case UNKNOWN:
case WEB_BROWSER:
default:
- return (isOwner() && !isForceBlocked(Permission.PUSH))
+ return
+ // We allow owner to delete refs even if they have no force-push rights. We forbid
+ // it if force push is blocked, though. See commit 40bd5741026863c99bea13eb5384bd27855c5e1b
+ (isOwner() && !isBlocked(Permission.PUSH, false, true))
|| canPushWithForce()
|| canPerform(Permission.DELETE)
|| projectControl.isAdmin();
@@ -267,158 +264,140 @@
return canPerform(Permission.FORGE_SERVER);
}
- private static class AllowedRange {
- private int allowMin;
- private int allowMax;
- private int blockMin = Integer.MIN_VALUE;
- private int blockMax = Integer.MAX_VALUE;
-
- void update(PermissionRule rule) {
- if (rule.isBlock()) {
- blockMin = Math.max(blockMin, rule.getMin());
- blockMax = Math.min(blockMax, rule.getMax());
- } else {
- allowMin = Math.min(allowMin, rule.getMin());
- allowMax = Math.max(allowMax, rule.getMax());
- }
- }
-
- int getAllowMin() {
- return allowMin;
- }
-
- int getAllowMax() {
- return allowMax;
- }
-
- int getBlockMin() {
- // ALLOW wins over BLOCK on the same project
- return Math.min(blockMin, allowMin - 1);
- }
-
- int getBlockMax() {
- // ALLOW wins over BLOCK on the same project
- return Math.max(blockMax, allowMax + 1);
- }
+ private static boolean isAllow(PermissionRule pr, boolean withForce) {
+ return pr.getAction() == Action.ALLOW && (pr.getForce() || !withForce);
}
- private PermissionRange toRange(String permissionName, List<PermissionRule> ruleList) {
- Map<ProjectRef, AllowedRange> ranges = new HashMap<>();
- for (PermissionRule rule : ruleList) {
- ProjectRef p = relevant.getRuleProps(rule);
- AllowedRange r = ranges.get(p);
- if (r == null) {
- r = new AllowedRange();
- ranges.put(p, r);
- }
- r.update(rule);
- }
- int allowMin = 0;
- int allowMax = 0;
- int blockMin = Integer.MIN_VALUE;
- int blockMax = Integer.MAX_VALUE;
- for (AllowedRange r : ranges.values()) {
- allowMin = Math.min(allowMin, r.getAllowMin());
- allowMax = Math.max(allowMax, r.getAllowMax());
- blockMin = Math.max(blockMin, r.getBlockMin());
- blockMax = Math.min(blockMax, r.getBlockMax());
- }
-
- // BLOCK wins over ALLOW across projects
- int min = Math.max(allowMin, blockMin + 1);
- int max = Math.min(allowMax, blockMax - 1);
- return new PermissionRange(permissionName, min, max);
+ private static boolean isBlock(PermissionRule pr, boolean withForce) {
+ // BLOCK with force specified is a weaker rule than without.
+ return pr.getAction() == Action.BLOCK && (!pr.getForce() || withForce);
}
- private boolean canPerform(String permissionName, boolean isChangeOwner) {
- List<PermissionRule> access = access(permissionName, isChangeOwner);
- List<PermissionRule> overridden = relevant.getOverridden(permissionName);
- Set<ProjectRef> allows = new HashSet<>();
- Set<ProjectRef> blocks = new HashSet<>();
- for (PermissionRule rule : access) {
- if (rule.isBlock() && !rule.getForce()) {
- blocks.add(relevant.getRuleProps(rule));
- } else {
- allows.add(relevant.getRuleProps(rule));
+ private PermissionRange toRange(String permissionName, boolean isChangeOwner) {
+ int blockAllowMin = Integer.MIN_VALUE, blockAllowMax = Integer.MAX_VALUE;
+
+ projectLoop:
+ for (List<Permission> ps : relevant.getBlockRules(permissionName)) {
+ boolean blockFound = false;
+ int projectBlockAllowMin = Integer.MIN_VALUE, projectBlockAllowMax = Integer.MAX_VALUE;
+
+ for (Permission p : ps) {
+ if (p.getExclusiveGroup()) {
+ for (PermissionRule pr : p.getRules()) {
+ if (pr.getAction() == Action.ALLOW && projectControl.match(pr, isChangeOwner)) {
+ // exclusive override, usually for a more specific ref.
+ continue projectLoop;
+ }
+ }
+ }
+
+ for (PermissionRule pr : p.getRules()) {
+ if (pr.getAction() == Action.BLOCK && projectControl.match(pr, isChangeOwner)) {
+ projectBlockAllowMin = pr.getMin() + 1;
+ projectBlockAllowMax = pr.getMax() - 1;
+ blockFound = true;
+ }
+ }
+
+ if (blockFound) {
+ for (PermissionRule pr : p.getRules()) {
+ if (pr.getAction() == Action.ALLOW && projectControl.match(pr, isChangeOwner)) {
+ projectBlockAllowMin = pr.getMin();
+ projectBlockAllowMax = pr.getMax();
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ blockAllowMin = Math.max(projectBlockAllowMin, blockAllowMin);
+ blockAllowMax = Math.min(projectBlockAllowMax, blockAllowMax);
+ }
+
+ int voteMin = 0, voteMax = 0;
+ for (PermissionRule pr : relevant.getAllowRules(permissionName)) {
+ if (pr.getAction() == PermissionRule.Action.ALLOW
+ && projectControl.match(pr, isChangeOwner)) {
+ // For votes, contrary to normal permissions, we aggregate all applicable rules.
+ voteMin = Math.min(voteMin, pr.getMin());
+ voteMax = Math.max(voteMax, pr.getMax());
}
}
- for (PermissionRule rule : overridden) {
- blocks.remove(relevant.getRuleProps(rule));
- }
- blocks.removeAll(allows);
- return blocks.isEmpty() && !allows.isEmpty();
+
+ return new PermissionRange(
+ permissionName, Math.max(voteMin, blockAllowMin), Math.min(voteMax, blockAllowMax));
}
- /** True if the user has force this permission. Works only for non labels. */
- private boolean canForcePerform(String permissionName) {
- List<PermissionRule> access = access(permissionName);
- List<PermissionRule> overridden = relevant.getOverridden(permissionName);
- Set<ProjectRef> allows = new HashSet<>();
- Set<ProjectRef> blocks = new HashSet<>();
- for (PermissionRule rule : access) {
- if (rule.isBlock()) {
- blocks.add(relevant.getRuleProps(rule));
- } else if (rule.getForce()) {
- allows.add(relevant.getRuleProps(rule));
+ private boolean isBlocked(String permissionName, boolean isChangeOwner, boolean withForce) {
+ // Permissions are ordered by (more general project, more specific ref). Because Permission
+ // does not have back pointers, we can't tell what ref-pattern or project each permission comes
+ // from.
+ List<List<Permission>> downwardPerProject = relevant.getBlockRules(permissionName);
+
+ projectLoop:
+ for (List<Permission> projectRules : downwardPerProject) {
+ boolean overrideFound = false;
+ for (Permission p : projectRules) {
+ // If this is an exclusive ALLOW, then block rules from the same project are ignored.
+ if (p.getExclusiveGroup()) {
+ for (PermissionRule pr : p.getRules()) {
+ if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
+ overrideFound = true;
+ break;
+ }
+ }
+ }
+ if (overrideFound) {
+ // Found an exclusive override, nothing further to do in this project.
+ continue projectLoop;
+ }
+
+ boolean blocked = false;
+ for (PermissionRule pr : p.getRules()) {
+ if (!withForce && pr.getForce()) {
+ // force on block rule only applies to withForce permission.
+ continue;
+ }
+
+ if (isBlock(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
+ blocked = true;
+ break;
+ }
+ }
+
+ if (blocked) {
+ // ALLOW in the same AccessSection (ie. in the same Permission) overrides the BLOCK.
+ for (PermissionRule pr : p.getRules()) {
+ if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
+ blocked = false;
+ break;
+ }
+ }
+ }
+
+ if (blocked) {
+ return true;
+ }
}
}
- for (PermissionRule rule : overridden) {
- if (rule.getForce()) {
- blocks.remove(relevant.getRuleProps(rule));
- }
- }
- blocks.removeAll(allows);
- return blocks.isEmpty() && !allows.isEmpty();
+
+ return false;
}
- /** True if for this permission force is blocked for the user. Works only for non labels. */
- private boolean isForceBlocked(String permissionName) {
- List<PermissionRule> access = access(permissionName);
- List<PermissionRule> overridden = relevant.getOverridden(permissionName);
- Set<ProjectRef> allows = new HashSet<>();
- Set<ProjectRef> blocks = new HashSet<>();
- for (PermissionRule rule : access) {
- if (rule.isBlock()) {
- blocks.add(relevant.getRuleProps(rule));
- } else if (rule.getForce()) {
- allows.add(relevant.getRuleProps(rule));
- }
- }
- for (PermissionRule rule : overridden) {
- if (rule.getForce()) {
- blocks.remove(relevant.getRuleProps(rule));
- }
- }
- blocks.removeAll(allows);
- return !blocks.isEmpty();
- }
-
- /** Rules for the given permission, or the empty list. */
- private List<PermissionRule> access(String permissionName) {
- return access(permissionName, false);
- }
-
- /** Rules for the given permission, or the empty list. */
- private List<PermissionRule> access(String permissionName, boolean isChangeOwner) {
- List<PermissionRule> rules = effective.get(permissionName);
- if (rules != null) {
- return rules;
+ /** True if the user has this permission. */
+ private boolean canPerform(String permissionName, boolean isChangeOwner, boolean withForce) {
+ if (isBlocked(permissionName, isChangeOwner, withForce)) {
+ return false;
}
- rules = relevant.getPermission(permissionName);
-
- List<PermissionRule> mine = new ArrayList<>(rules.size());
- for (PermissionRule rule : rules) {
- if (projectControl.match(rule, isChangeOwner)) {
- mine.add(rule);
+ for (PermissionRule pr : relevant.getAllowRules(permissionName)) {
+ if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
+ return true;
}
}
- if (mine.isEmpty()) {
- mine = Collections.emptyList();
- }
- effective.put(permissionName, mine);
- return mine;
+ return false;
}
private class ForRefImpl extends ForRef {
diff --git a/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 740e8d3..effd51a 100644
--- a/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -34,6 +34,7 @@
import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
import com.google.gerrit.extensions.systemstatus.ServerInformation;
import com.google.gerrit.extensions.webui.WebUiPlugin;
+import com.google.gerrit.index.IndexCollection;
import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.util.PluginRequestContext;
import com.google.gerrit.server.util.RequestContext;
@@ -568,7 +569,7 @@
Class<?> type = key.getTypeLiteral().getRawType();
if (LifecycleListener.class.isAssignableFrom(type)
// This is needed for secondary index to work from plugin listeners
- && !is("com.google.gerrit.server.index.IndexCollection", type)) {
+ && !IndexCollection.class.isAssignableFrom(type)) {
return false;
}
if (StartPluginListener.class.isAssignableFrom(type)) {
diff --git a/java/com/google/gerrit/server/project/Reachable.java b/java/com/google/gerrit/server/project/Reachable.java
index 42389d3..67d8f70 100644
--- a/java/com/google/gerrit/server/project/Reachable.java
+++ b/java/com/google/gerrit/server/project/Reachable.java
@@ -16,9 +16,14 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.change.IncludedInResolver;
-import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
+import com.google.gerrit.server.permissions.PermissionBackendException;
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.Map;
@@ -35,22 +40,31 @@
* Report whether a commit is reachable from a set of commits. This is used for checking if a user
* has read permissions on a commit.
*/
+@Singleton
public class Reachable {
- private final VisibleRefFilter.Factory refFilter;
private static final Logger log = LoggerFactory.getLogger(Reachable.class);
+ private final PermissionBackend permissionBackend;
+ private final Provider<CurrentUser> user;
+
@Inject
- Reachable(VisibleRefFilter.Factory refFilter) {
- this.refFilter = refFilter;
+ Reachable(PermissionBackend permissionBackend, Provider<CurrentUser> user) {
+ this.permissionBackend = permissionBackend;
+ this.user = user;
}
/** @return true if a commit is reachable from a given set of refs. */
public boolean fromRefs(
ProjectState state, Repository repo, RevCommit commit, Map<String, Ref> refs) {
try (RevWalk rw = new RevWalk(repo)) {
- Map<String, Ref> filtered = refFilter.create(state, repo).filter(refs, true);
+ // TODO(hiesel) Convert interface to Project.NameKey
+ Map<String, Ref> filtered =
+ permissionBackend
+ .user(user)
+ .project(state.getNameKey())
+ .filter(refs, repo, RefFilterOptions.builder().setFilterTagsSeparately(true).build());
return IncludedInResolver.includedInAny(repo, rw, commit, filtered.values());
- } catch (IOException e) {
+ } catch (IOException | PermissionBackendException e) {
log.error(
String.format(
"Cannot verify permissions to commit object %s in repository %s",
diff --git a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index a8434b9..c1da015 100644
--- a/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -24,7 +24,6 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.Emails;
@@ -87,7 +86,7 @@
}
public interface Factory {
- SubmitRuleEvaluator create(CurrentUser user, ChangeData cd);
+ SubmitRuleEvaluator create(ChangeData cd);
}
private final AccountCache accountCache;
@@ -99,7 +98,6 @@
private SubmitRuleOptions.Builder optsBuilder = SubmitRuleOptions.builder();
private SubmitRuleOptions opts;
private Change change;
- private CurrentUser user;
private PatchSet patchSet;
private boolean logErrors = true;
private long reductionsConsumed;
@@ -113,13 +111,11 @@
Accounts accounts,
Emails emails,
ProjectCache projectCache,
- @Assisted CurrentUser user,
@Assisted ChangeData cd) {
this.accountCache = accountCache;
this.accounts = accounts;
this.emails = emails;
this.projectCache = projectCache;
- this.user = user;
this.cd = cd;
}
@@ -229,11 +225,7 @@
try {
results =
evaluateImpl(
- "locate_submit_rule",
- "can_submit",
- "locate_submit_filter",
- "filter_submit_results",
- user);
+ "locate_submit_rule", "can_submit", "locate_submit_filter", "filter_submit_results");
} catch (RuleEvalException e) {
return ruleError(e.getMessage(), e);
}
@@ -391,11 +383,7 @@
"locate_submit_type",
"get_submit_type",
"locate_submit_type_filter",
- "filter_submit_type_results",
- // Do not include current user in submit type evaluation. This is used
- // for mergeability checks, which are stored persistently and so must
- // have a consistent view of the submit type.
- null);
+ "filter_submit_type_results");
} catch (RuleEvalException e) {
return typeError(e.getMessage(), e);
}
@@ -460,10 +448,9 @@
String userRuleLocatorName,
String userRuleWrapperName,
String filterRuleLocatorName,
- String filterRuleWrapperName,
- CurrentUser user)
+ String filterRuleWrapperName)
throws RuleEvalException {
- PrologEnvironment env = getPrologEnvironment(user);
+ PrologEnvironment env = getPrologEnvironment();
try {
Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm());
List<Term> results = new ArrayList<>();
@@ -507,7 +494,7 @@
}
}
- private PrologEnvironment getPrologEnvironment(CurrentUser user) throws RuleEvalException {
+ private PrologEnvironment getPrologEnvironment() throws RuleEvalException {
PrologEnvironment env;
try {
if (opts.rule() == null) {
@@ -529,9 +516,6 @@
env.set(StoredValues.EMAILS, emails);
env.set(StoredValues.REVIEW_DB, cd.db());
env.set(StoredValues.CHANGE_DATA, cd);
- if (user != null) {
- env.set(StoredValues.CURRENT_USER, user);
- }
env.set(StoredValues.PROJECT_STATE, projectState);
return env;
}
diff --git a/java/com/google/gerrit/server/project/testing/Util.java b/java/com/google/gerrit/server/project/testing/Util.java
index 2851e81..b80d74c 100644
--- a/java/com/google/gerrit/server/project/testing/Util.java
+++ b/java/com/google/gerrit/server/project/testing/Util.java
@@ -80,6 +80,19 @@
return grant(project, permissionName, rule, ref);
}
+ public static PermissionRule allowExclusive(
+ ProjectConfig project,
+ String permissionName,
+ int min,
+ int max,
+ AccountGroup.UUID group,
+ String ref) {
+ PermissionRule rule = newRule(project, group);
+ rule.setMin(min);
+ rule.setMax(max);
+ return grant(project, permissionName, rule, ref, true);
+ }
+
public static PermissionRule block(
ProjectConfig project,
String permissionName,
diff --git a/java/com/google/gerrit/server/query/account/AccountPredicates.java b/java/com/google/gerrit/server/query/account/AccountPredicates.java
index 22df2ce..e643470 100644
--- a/java/com/google/gerrit/server/query/account/AccountPredicates.java
+++ b/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -45,7 +45,7 @@
preds.add(id(new Account.Id(id)));
}
if (canSeeSecondaryEmails) {
- preds.add(equalsNameIcludingSecondaryEmails(query));
+ preds.add(equalsNameIncludingSecondaryEmails(query));
} else {
if (schema.hasField(AccountField.NAME_PART_NO_SECONDARY_EMAIL)) {
preds.add(equalsName(query));
@@ -84,7 +84,7 @@
AccountField.PREFERRED_EMAIL_EXACT, AccountQueryBuilder.FIELD_PREFERRED_EMAIL_EXACT, email);
}
- public static Predicate<AccountState> equalsNameIcludingSecondaryEmails(String name) {
+ public static Predicate<AccountState> equalsNameIncludingSecondaryEmails(String name) {
return new AccountPredicate(
AccountField.NAME_PART, AccountQueryBuilder.FIELD_NAME, name.toLowerCase());
}
diff --git a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index 055b423..8f6ec8b 100644
--- a/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -142,7 +142,7 @@
public Predicate<AccountState> name(String name)
throws PermissionBackendException, QueryParseException {
if (canSeeSecondaryEmails()) {
- return AccountPredicates.equalsNameIcludingSecondaryEmails(name);
+ return AccountPredicates.equalsNameIncludingSecondaryEmails(name);
}
if (args.schema().hasField(AccountField.NAME_PART_NO_SECONDARY_EMAIL)) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 73b212c..e11d51e 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -905,17 +905,13 @@
return messages;
}
- public List<SubmitRecord> submitRecords(SubmitRuleOptions options) throws OrmException {
+ public List<SubmitRecord> submitRecords(SubmitRuleOptions options) {
List<SubmitRecord> records = submitRecords.get(options);
if (records == null) {
if (!lazyLoad) {
return Collections.emptyList();
}
- records =
- submitRuleEvaluatorFactory
- .create(userFactory.create(change().getOwner()), this)
- .setOptions(options)
- .evaluate();
+ records = submitRuleEvaluatorFactory.create(this).setOptions(options).evaluate();
submitRecords.put(options, records);
}
return records;
@@ -930,12 +926,9 @@
submitRecords.put(options, records);
}
- public SubmitTypeRecord submitTypeRecord() throws OrmException {
+ public SubmitTypeRecord submitTypeRecord() {
if (submitTypeRecord == null) {
- submitTypeRecord =
- submitRuleEvaluatorFactory
- .create(userFactory.create(change().getOwner()), this)
- .getSubmitType();
+ submitTypeRecord = submitRuleEvaluatorFactory.create(this).getSubmitType();
}
return submitTypeRecord;
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index a28cd9d..1d24377 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -21,6 +21,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Enums;
+import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -57,7 +58,6 @@
import com.google.gerrit.server.change.ChangeTriplet;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.strategy.SubmitDryRun;
import com.google.gerrit.server.index.change.ChangeField;
@@ -91,7 +91,6 @@
import java.util.regex.Pattern;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Repository;
/** Parses a query string meant to be applied to change objects. */
@@ -214,7 +213,6 @@
final Provider<ReviewDb> db;
final StarredChangesUtil starredChangesUtil;
final SubmitDryRun submitDryRun;
- final boolean allowsDrafts;
final GroupMembers groupMembers;
private final Provider<CurrentUser> self;
@@ -247,7 +245,6 @@
IndexConfig indexConfig,
StarredChangesUtil starredChangesUtil,
AccountCache accountCache,
- @GerritServerConfig Config cfg,
NotesMigration notesMigration,
GroupMembers groupMembers) {
this(
@@ -276,7 +273,6 @@
indexConfig,
starredChangesUtil,
accountCache,
- cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true),
notesMigration,
groupMembers);
}
@@ -307,7 +303,6 @@
IndexConfig indexConfig,
StarredChangesUtil starredChangesUtil,
AccountCache accountCache,
- boolean allowsDrafts,
NotesMigration notesMigration,
GroupMembers groupMembers) {
this.db = db;
@@ -334,7 +329,6 @@
this.indexConfig = indexConfig;
this.starredChangesUtil = starredChangesUtil;
this.accountCache = accountCache;
- this.allowsDrafts = allowsDrafts;
this.hasOperands = hasOperands;
this.notesMigration = notesMigration;
this.groupMembers = groupMembers;
@@ -367,7 +361,6 @@
indexConfig,
starredChangesUtil,
accountCache,
- allowsDrafts,
notesMigration,
groupMembers);
}
@@ -526,9 +519,9 @@
}
// for plugins the value will be operandName_pluginName
- String[] names = value.split("_");
- if (names.length == 2) {
- ChangeHasOperandFactory op = args.hasOperands.get(names[1], names[0]);
+ List<String> names = Lists.newArrayList(Splitter.on('_').split(value));
+ if (names.size() == 2) {
+ ChangeHasOperandFactory op = args.hasOperands.get(names.get(1), names.get(0));
if (op != null) {
return op.create(this);
}
@@ -729,12 +722,12 @@
// Special case: votes by owners can be tracked with ",owner":
// label:Code-Review+2,owner
// label:Code-Review+2,user=owner
- String[] splitReviewer = name.split(",", 2);
- name = splitReviewer[0]; // remove all but the vote piece, e.g.'CodeReview=1'
+ List<String> splitReviewer = Lists.newArrayList(Splitter.on(',').limit(2).split(name));
+ name = splitReviewer.get(0); // remove all but the vote piece, e.g.'CodeReview=1'
- if (splitReviewer.length == 2) {
+ if (splitReviewer.size() == 2) {
// process the user/group piece
- PredicateArgs lblArgs = new PredicateArgs(splitReviewer[1]);
+ PredicateArgs lblArgs = new PredicateArgs(splitReviewer.get(1));
for (Map.Entry<String, String> pair : lblArgs.keyValue.entrySet()) {
if (pair.getKey().equalsIgnoreCase(ARG_ID_USER)) {
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 185517a..64c6208 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -24,7 +24,6 @@
import com.google.gerrit.reviewdb.client.PatchSet;
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.config.TrackingFooters;
import com.google.gerrit.server.data.ChangeAttribute;
import com.google.gerrit.server.data.PatchSetAttribute;
@@ -82,7 +81,6 @@
private final ChangeQueryProcessor queryProcessor;
private final EventFactory eventFactory;
private final TrackingFooters trackingFooters;
- private final CurrentUser user;
private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
private OutputFormat outputFormat = OutputFormat.TEXT;
@@ -107,7 +105,6 @@
ChangeQueryProcessor queryProcessor,
EventFactory eventFactory,
TrackingFooters trackingFooters,
- CurrentUser user,
SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory) {
this.db = db;
this.repoManager = repoManager;
@@ -115,7 +112,6 @@
this.queryProcessor = queryProcessor;
this.eventFactory = eventFactory;
this.trackingFooters = trackingFooters;
- this.user = user;
this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
}
@@ -254,7 +250,7 @@
if (includeSubmitRecords) {
eventFactory.addSubmitRecords(
- c, submitRuleEvaluatorFactory.create(user, d).setAllowClosed(true).evaluate());
+ c, submitRuleEvaluatorFactory.create(d).setAllowClosed(true).evaluate());
}
if (includeCommitMessage) {
diff --git a/java/com/google/gerrit/server/query/change/PredicateArgs.java b/java/com/google/gerrit/server/query/change/PredicateArgs.java
index ad7a57d..ad7917e 100644
--- a/java/com/google/gerrit/server/query/change/PredicateArgs.java
+++ b/java/com/google/gerrit/server/query/change/PredicateArgs.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.query.change;
+import com.google.common.base.Splitter;
import com.google.gerrit.index.query.QueryParseException;
import java.util.ArrayList;
import java.util.HashMap;
@@ -45,18 +46,16 @@
positional = new ArrayList<>();
keyValue = new HashMap<>();
- String[] splitArgs = args.split(",");
+ for (String arg : Splitter.on(',').split(args)) {
+ List<String> splitKeyValue = Splitter.on('=').splitToList(arg);
- for (String arg : splitArgs) {
- String[] splitKeyValue = arg.split("=");
-
- if (splitKeyValue.length == 1) {
- positional.add(splitKeyValue[0]);
- } else if (splitKeyValue.length == 2) {
- if (!keyValue.containsKey(splitKeyValue[0])) {
- keyValue.put(splitKeyValue[0], splitKeyValue[1]);
+ if (splitKeyValue.size() == 1) {
+ positional.add(splitKeyValue.get(0));
+ } else if (splitKeyValue.size() == 2) {
+ if (!keyValue.containsKey(splitKeyValue.get(0))) {
+ keyValue.put(splitKeyValue.get(0), splitKeyValue.get(1));
} else {
- throw new QueryParseException("Duplicate key " + splitKeyValue[0]);
+ throw new QueryParseException("Duplicate key " + splitKeyValue.get(0));
}
} else {
throw new QueryParseException("invalid arg " + arg);
diff --git a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
index 057cc44..296dc17 100644
--- a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -20,7 +20,6 @@
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.query.LimitPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryBuilder;
@@ -32,9 +31,6 @@
import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.group.InternalGroup;
-import com.google.gerrit.server.index.group.GroupField;
-import com.google.gerrit.server.index.group.GroupIndex;
-import com.google.gerrit.server.index.group.GroupIndexCollection;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.IOException;
@@ -56,18 +52,12 @@
new QueryBuilder.Definition<>(GroupQueryBuilder.class);
public static class Arguments {
- final GroupIndex groupIndex;
final GroupCache groupCache;
final GroupBackend groupBackend;
final AccountResolver accountResolver;
@Inject
- Arguments(
- GroupIndexCollection groupIndexCollection,
- GroupCache groupCache,
- GroupBackend groupBackend,
- AccountResolver accountResolver) {
- this.groupIndex = groupIndexCollection.getSearchIndex();
+ Arguments(GroupCache groupCache, GroupBackend groupBackend, AccountResolver accountResolver) {
this.groupCache = groupCache;
this.groupBackend = groupBackend;
this.accountResolver = accountResolver;
@@ -144,10 +134,6 @@
@Operator
public Predicate<InternalGroup> member(String query)
throws QueryParseException, OrmException, ConfigInvalidException, IOException {
- if (isFieldAbsentFromIndex(GroupField.MEMBER)) {
- throw getExceptionForUnsupportedOperator("member");
- }
-
Set<Account.Id> accounts = parseAccount(query);
List<Predicate<InternalGroup>> predicates =
accounts.stream().map(GroupPredicates::member).collect(toImmutableList());
@@ -156,10 +142,6 @@
@Operator
public Predicate<InternalGroup> subgroup(String query) throws QueryParseException {
- if (isFieldAbsentFromIndex(GroupField.SUBGROUP)) {
- throw getExceptionForUnsupportedOperator("subgroup");
- }
-
AccountGroup.UUID groupUuid = parseGroup(query);
return GroupPredicates.subgroup(groupUuid);
}
@@ -173,15 +155,6 @@
return new LimitPredicate<>(FIELD_LIMIT, limit);
}
- private boolean isFieldAbsentFromIndex(FieldDef<InternalGroup, ?> field) {
- return !args.groupIndex.getSchema().hasField(field);
- }
-
- private static QueryParseException getExceptionForUnsupportedOperator(String operatorName) {
- return new QueryParseException(
- String.format("'%s' operator is not supported by group index version", operatorName));
- }
-
private Set<Account.Id> parseAccount(String nameOrEmail)
throws QueryParseException, OrmException, IOException, ConfigInvalidException {
Set<Account.Id> foundAccounts = args.accountResolver.findAll(nameOrEmail);
diff --git a/java/com/google/gerrit/server/restapi/account/CreateAccount.java b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
index 29c331f..31bf93f 100644
--- a/java/com/google/gerrit/server/restapi/account/CreateAccount.java
+++ b/java/com/google/gerrit/server/restapi/account/CreateAccount.java
@@ -119,7 +119,7 @@
throw new BadRequestException("username must match URL");
}
- if (!username.matches(ExternalId.USER_NAME_PATTERN_REGEX)) {
+ if (!ExternalId.isValidUsername(username)) {
throw new BadRequestException(
"Username '" + username + "' must contain only letters, numbers, _, - or .");
}
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
index 3d103ec..61ba8cc 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteExternalIds.java
@@ -41,6 +41,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
@@ -81,7 +82,7 @@
.collect(toMap(i -> i.key(), i -> i));
List<ExternalId> toDelete = new ArrayList<>();
- ExternalId.Key last = resource.getUser().getLastLoginExternalIdKey();
+ Optional<ExternalId.Key> last = resource.getUser().getLastLoginExternalIdKey();
for (String externalIdStr : extIds) {
ExternalId id = externalIdMap.get(ExternalId.Key.parse(externalIdStr));
@@ -91,7 +92,7 @@
}
if ((!id.isScheme(SCHEME_USERNAME))
- && ((last == null) || (!last.get().equals(id.key().get())))) {
+ && (!last.isPresent() || (!last.get().equals(id.key())))) {
toDelete.add(id);
} else {
throw new ResourceConflictException(
diff --git a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
index 8e456a2..284c7f9 100644
--- a/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
+++ b/java/com/google/gerrit/server/restapi/account/GetExternalIds.java
@@ -37,6 +37,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
@Singleton
public class GetExternalIds implements RestReadView<AccountResource> {
@@ -78,8 +79,8 @@
// establish this web session, and if only if an identity was
// actually used to establish this web session.
if (!id.isScheme(SCHEME_USERNAME)) {
- ExternalId.Key last = resource.getUser().getLastLoginExternalIdKey();
- info.canDelete = toBoolean(last == null || !last.get().equals(info.identity));
+ Optional<ExternalId.Key> last = resource.getUser().getLastLoginExternalIdKey();
+ info.canDelete = toBoolean(!last.isPresent() || !last.get().get().equals(info.identity));
}
result.add(info);
}
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 9bbd56f..499cbb6 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -16,6 +16,7 @@
import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
+import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
@@ -240,14 +241,23 @@
AccountState accountState = me.state();
GeneralPreferencesInfo info = accountState.getGeneralPreferences();
- ObjectId treeId = mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree();
- ObjectId id = ChangeIdUtil.computeChangeId(treeId, mergeTip, author, author, input.subject);
- String commitMessage = ChangeIdUtil.insertId(input.subject, id);
+ // Add a Change-Id line if there isn't already one
+ String commitMessage = input.subject;
+ if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
+ ObjectId treeId = mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree();
+ ObjectId id = ChangeIdUtil.computeChangeId(treeId, mergeTip, author, author, commitMessage);
+ commitMessage = ChangeIdUtil.insertId(commitMessage, id);
+ }
+
if (Boolean.TRUE.equals(info.signedOffBy)) {
- commitMessage +=
- String.format(
- "%s%s",
- SIGNED_OFF_BY_TAG, accountState.getAccount().getNameEmail(anonymousCowardName));
+ commitMessage =
+ Joiner.on("\n")
+ .join(
+ commitMessage.trim(),
+ String.format(
+ "%s%s",
+ SIGNED_OFF_BY_TAG,
+ accountState.getAccount().getNameEmail(anonymousCowardName)));
}
RevCommit c;
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeOp.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeOp.java
index 1853853..9658fb4 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeOp.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeOp.java
@@ -38,15 +38,10 @@
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
-import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevWalk;
class DeleteChangeOp implements BatchUpdateOp {
- static boolean allowDrafts(Config cfg) {
- return cfg.getBoolean("change", "allowDrafts", true);
- }
-
private final PatchSetUtil psUtil;
private final StarredChangesUtil starredChangesUtil;
private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore;
diff --git a/java/com/google/gerrit/server/restapi/change/Mergeable.java b/java/com/google/gerrit/server/restapi/change/Mergeable.java
index 7a2f424..47d76b2 100644
--- a/java/com/google/gerrit/server/restapi/change/Mergeable.java
+++ b/java/com/google/gerrit/server/restapi/change/Mergeable.java
@@ -26,7 +26,6 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.change.MergeabilityCache;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.git.BranchOrderSection;
@@ -113,7 +112,7 @@
}
ChangeData cd = changeDataFactory.create(db.get(), resource.getNotes());
- result.submitType = getSubmitType(resource.getUser(), cd, ps);
+ result.submitType = getSubmitType(cd, ps);
try (Repository git = gitManager.openRepository(change.getProject())) {
ObjectId commit = toId(ps);
@@ -145,10 +144,9 @@
return result;
}
- private SubmitType getSubmitType(CurrentUser user, ChangeData cd, PatchSet patchSet)
- throws OrmException {
+ private SubmitType getSubmitType(ChangeData cd, PatchSet patchSet) throws OrmException {
SubmitTypeRecord rec =
- submitRuleEvaluatorFactory.create(user, cd).setPatchSet(patchSet).getSubmitType();
+ submitRuleEvaluatorFactory.create(cd).setPatchSet(patchSet).getSubmitType();
if (rec.status != SubmitTypeRecord.Status.OK) {
throw new OrmException("Submit type rule failed: " + rec);
}
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 8190735..8e78ec0 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -135,7 +135,6 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -334,11 +333,9 @@
// User posting this review isn't currently in the reviewer or CC list,
// isn't being explicitly added, and isn't voting on any label.
// Automatically CC them on this change so they receive replies.
- Optional<PostReviewers.Addition> selfAddition =
+ PostReviewers.Addition selfAddition =
postReviewers.ccCurrentUser(revision.getUser(), revision);
- if (selfAddition.isPresent()) {
- bu.addOp(revision.getChange().getId(), selfAddition.get().op);
- }
+ bu.addOp(revision.getChange().getId(), selfAddition.op);
}
// Add WorkInProgressOp if requested.
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index 102bf21..4ff3862 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -80,7 +80,6 @@
import java.text.MessageFormat;
import java.util.Collection;
import java.util.HashSet;
-import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@@ -209,18 +208,15 @@
return addByEmail(reviewer, rsrc, state, notify, accountsToNotify);
}
- Optional<Addition> ccCurrentUser(CurrentUser user, RevisionResource revision) {
- return user.getUserName()
- .map(
- u ->
- new Addition(
- u,
- revision.getChangeResource(),
- ImmutableSet.of(user.getAccountId()),
- null,
- CC,
- NotifyHandling.NONE,
- ImmutableListMultimap.of()));
+ Addition ccCurrentUser(CurrentUser user, RevisionResource revision) {
+ return new Addition(
+ user.getUserName().orElse(null),
+ revision.getChangeResource(),
+ ImmutableSet.of(user.getAccountId()),
+ null,
+ CC,
+ NotifyHandling.NONE,
+ ImmutableListMultimap.of());
}
@Nullable
@@ -451,7 +447,7 @@
result.ccs = Lists.newArrayListWithCapacity(opResult.addedCCs().size());
for (Account.Id accountId : opResult.addedCCs()) {
IdentifiedUser u = identifiedUserFactory.create(accountId);
- result.ccs.add(json.format(caller, new ReviewerInfo(accountId.get()), perm.user(u), cd));
+ result.ccs.add(json.format(new ReviewerInfo(accountId.get()), perm.user(u), cd));
}
accountLoaderFactory.create(true).fill(result.ccs);
for (Address a : reviewersByEmail) {
@@ -464,7 +460,6 @@
IdentifiedUser u = identifiedUserFactory.create(psa.getAccountId());
result.reviewers.add(
json.format(
- caller,
new ReviewerInfo(psa.getAccountId().get()),
perm.user(u),
cd,
diff --git a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
index a78f9b5..303401c 100644
--- a/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
+++ b/java/com/google/gerrit/server/restapi/change/RelatedChangesSorter.java
@@ -275,5 +275,15 @@
public int hashCode() {
return Objects.hash(patchSet().getId(), commit());
}
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof PatchSetData)) {
+ return false;
+ }
+ PatchSetData o = (PatchSetData) obj;
+ return Objects.equals(patchSet().getId(), o.patchSet().getId())
+ && Objects.equals(commit(), o.commit());
+ }
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerJson.java b/java/com/google/gerrit/server/restapi/change/ReviewerJson.java
index 228eb47..ef5b432 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerJson.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerJson.java
@@ -27,7 +27,6 @@
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.CurrentUser;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.permissions.LabelPermission;
@@ -79,7 +78,6 @@
}
ReviewerInfo info =
format(
- rsrc.getChangeResource().getUser(),
new ReviewerInfo(rsrc.getReviewerUser().getAccountId().get()),
permissionBackend.user(rsrc.getReviewerUser()).database(db).change(cd),
cd);
@@ -95,12 +93,10 @@
return format(ImmutableList.<ReviewerResource>of(rsrc));
}
- public ReviewerInfo format(
- CurrentUser user, ReviewerInfo out, PermissionBackend.ForChange perm, ChangeData cd)
+ public ReviewerInfo format(ReviewerInfo out, PermissionBackend.ForChange perm, ChangeData cd)
throws OrmException, PermissionBackendException {
PatchSet.Id psId = cd.change().currentPatchSetId();
return format(
- user,
out,
perm,
cd,
@@ -109,7 +105,6 @@
}
public ReviewerInfo format(
- CurrentUser user,
ReviewerInfo out,
PermissionBackend.ForChange perm,
ChangeData cd,
@@ -129,7 +124,7 @@
// do not exist in the DB.
PatchSet ps = cd.currentPatchSet();
if (ps != null) {
- for (SubmitRecord rec : submitRuleEvaluatorFactory.create(user, cd).evaluate()) {
+ for (SubmitRecord rec : submitRuleEvaluatorFactory.create(cd).evaluate()) {
if (rec.labels == null) {
continue;
}
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index a27d376..5f6b088 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -130,24 +130,19 @@
.get()
.suggestReviewers(
projectState.getNameKey(),
- changeNotes.getChangeId(),
+ changeNotes != null ? changeNotes.getChangeId() : null,
query,
reviewerScores.keySet()));
- String pluginWeight =
- config.getString(
- "addReviewer", plugin.getPluginName() + "-" + plugin.getExportName(), "weight");
+ String key = plugin.getPluginName() + "-" + plugin.getExportName();
+ String pluginWeight = config.getString("addReviewer", key, "weight");
if (Strings.isNullOrEmpty(pluginWeight)) {
pluginWeight = "1";
}
+ log.debug("weight for {}: {}", key, pluginWeight);
try {
weights.add(Double.parseDouble(pluginWeight));
} catch (NumberFormatException e) {
- log.error(
- "Exception while parsing weight for "
- + plugin.getPluginName()
- + "-"
- + plugin.getExportName(),
- e);
+ log.error("Exception while parsing weight for {}", key, e);
weights.add(1d);
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 7a2a148..38ed756 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -65,6 +65,8 @@
import java.util.Objects;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class ReviewersUtil {
@Singleton
@@ -110,6 +112,8 @@
}
}
+ private static final Logger log = LoggerFactory.getLogger(ReviewersUtil.class);
+
// Generate a candidate list at 2x the size of what the user wants to see to
// give the ranking algorithm a good set of candidates it can work with
private static final int CANDIDATE_LIST_MULTIPLIER = 2;
@@ -163,20 +167,29 @@
VisibilityControl visibilityControl,
boolean excludeGroups)
throws IOException, OrmException, ConfigInvalidException, PermissionBackendException {
+ CurrentUser currentUser = self.get();
+ log.debug(
+ "Suggesting reviewers for change {} to user {}.",
+ changeNotes.getChangeId().get(),
+ currentUser.getLoggableName());
String query = suggestReviewers.getQuery();
+ log.debug("Query: {}", query);
int limit = suggestReviewers.getLimit();
if (!suggestReviewers.getSuggestAccounts()) {
+ log.debug("Reviewer suggestion is disabled.");
return Collections.emptyList();
}
List<Account.Id> candidateList = new ArrayList<>();
if (!Strings.isNullOrEmpty(query)) {
candidateList = suggestAccounts(suggestReviewers);
+ log.debug("Candidate list: {}", candidateList);
}
List<Account.Id> sortedRecommendations =
recommendAccounts(changeNotes, suggestReviewers, projectState, candidateList);
+ log.debug("Sorted recommendations: {}", sortedRecommendations);
// Filter accounts by visibility and enforce limit
List<Account.Id> filteredRecommendations = new ArrayList<>();
@@ -192,20 +205,40 @@
}
}
}
+ log.debug("Filtered recommendations: {}", filteredRecommendations);
- List<SuggestedReviewerInfo> suggestedReviewer = loadAccounts(filteredRecommendations);
- if (!excludeGroups && suggestedReviewer.size() < limit && !Strings.isNullOrEmpty(query)) {
+ List<SuggestedReviewerInfo> suggestedReviewers = loadAccounts(filteredRecommendations);
+ if (!excludeGroups && suggestedReviewers.size() < limit && !Strings.isNullOrEmpty(query)) {
// Add groups at the end as individual accounts are usually more
// important.
- suggestedReviewer.addAll(
+ suggestedReviewers.addAll(
suggestAccountGroups(
- suggestReviewers, projectState, visibilityControl, limit - suggestedReviewer.size()));
+ suggestReviewers,
+ projectState,
+ visibilityControl,
+ limit - suggestedReviewers.size()));
}
- if (suggestedReviewer.size() <= limit) {
- return suggestedReviewer;
+ if (suggestedReviewers.size() > limit) {
+ suggestedReviewers = suggestedReviewers.subList(0, limit);
+ log.debug("Limited suggested reviewers to {} accounts.", limit);
}
- return suggestedReviewer.subList(0, limit);
+ log.debug(
+ "Suggested reviewers: {}",
+ suggestedReviewers
+ .stream()
+ .map(
+ r -> {
+ if (r.account != null) {
+ return "a/" + r.account._accountId;
+ } else if (r.group != null) {
+ return "g/" + r.group.id;
+ } else {
+ return "";
+ }
+ })
+ .collect(toList()));
+ return suggestedReviewers;
}
private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers) throws OrmException {
diff --git a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
index 4dc5b06..4033d10 100644
--- a/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/SuggestChangeReviewers.java
@@ -19,14 +19,16 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.IdentifiedUser.GenericFactory;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.config.GerritServerConfig;
-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.permissions.RefPermission;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.restapi.change.ReviewersUtil.VisibilityControl;
import com.google.gwtorm.server.OrmException;
@@ -48,6 +50,7 @@
)
boolean excludeGroups;
+ private final PermissionBackend permissionBackend;
private final Provider<CurrentUser> self;
private final ProjectCache projectCache;
@@ -56,11 +59,13 @@
AccountVisibility av,
GenericFactory identifiedUserFactory,
Provider<ReviewDb> dbProvider,
+ PermissionBackend permissionBackend,
Provider<CurrentUser> self,
@GerritServerConfig Config cfg,
ReviewersUtil reviewersUtil,
ProjectCache projectCache) {
super(av, identifiedUserFactory, dbProvider, cfg, reviewersUtil);
+ this.permissionBackend = permissionBackend;
this.self = self;
this.projectCache = projectCache;
}
@@ -81,11 +86,15 @@
}
private VisibilityControl getVisibility(ChangeResource rsrc) {
- // Use the destination reference, not the change, as drafts may deny
- // anyone who is not already a reviewer.
- return account -> {
- IdentifiedUser who = identifiedUserFactory.create(account);
- return rsrc.permissions().user(who).testOrFalse(ChangePermission.READ);
+ // Use the destination reference, not the change, as private changes deny anyone who is not
+ // already a reviewer.
+ PermissionBackend.ForRef perm = permissionBackend.user(self).ref(rsrc.getChange().getDest());
+ return new VisibilityControl() {
+ @Override
+ public boolean isVisibleTo(Account.Id account) throws OrmException {
+ IdentifiedUser who = identifiedUserFactory.create(account);
+ return perm.user(who).testOrFalse(RefPermission.READ);
+ }
};
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java b/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
index efb0f4d..eb356ed 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitRule.java
@@ -71,8 +71,7 @@
}
input.filters = MoreObjects.firstNonNull(input.filters, filters);
SubmitRuleEvaluator evaluator =
- submitRuleEvaluatorFactory.create(
- rsrc.getUser(), changeDataFactory.create(db.get(), rsrc.getNotes()));
+ submitRuleEvaluatorFactory.create(changeDataFactory.create(db.get(), rsrc.getNotes()));
List<SubmitRecord> records =
evaluator
diff --git a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
index 2782a66..d8d5d0a 100644
--- a/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
+++ b/java/com/google/gerrit/server/restapi/change/TestSubmitType.java
@@ -65,8 +65,7 @@
}
input.filters = MoreObjects.firstNonNull(input.filters, filters);
SubmitRuleEvaluator evaluator =
- submitRuleEvaluatorFactory.create(
- rsrc.getUser(), changeDataFactory.create(db.get(), rsrc.getNotes()));
+ submitRuleEvaluatorFactory.create(changeDataFactory.create(db.get(), rsrc.getNotes()));
SubmitTypeRecord rec =
evaluator
diff --git a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
index f31277d..3fb4ed1 100644
--- a/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
+++ b/java/com/google/gerrit/server/restapi/config/GetServerInfo.java
@@ -231,7 +231,6 @@
private ChangeConfigInfo getChangeInfo(Config cfg) {
ChangeConfigInfo info = new ChangeConfigInfo();
info.allowBlame = toBoolean(cfg.getBoolean("change", "allowBlame", true));
- info.allowDrafts = toBoolean(cfg.getBoolean("change", "allowDrafts", true));
boolean hasAssigneeInIndex =
indexes.getSearchIndex().getSchema().hasField(ChangeField.ASSIGNEE);
info.showAssigneeInChangesTable =
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java
index 224a014..f65b29f 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -190,7 +190,7 @@
}
private Optional<Account> createAccountByLdap(String user) throws IOException {
- if (!user.matches(ExternalId.USER_NAME_PATTERN_REGEX)) {
+ if (!ExternalId.isValidUsername(user)) {
return Optional.empty();
}
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index d0eca26..a0c88f2 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -139,12 +139,12 @@
throw new BadRequestException("name must match URL");
}
- AccountGroup.Id ownerId = owner(input);
+ AccountGroup.UUID ownerUuid = owner(input);
CreateGroupArgs args = new CreateGroupArgs();
args.setGroupName(name);
args.groupDescription = Strings.emptyToNull(input.description);
args.visibleToAll = MoreObjects.firstNonNull(input.visibleToAll, defaultVisibleToAll);
- args.ownerGroupId = ownerId;
+ args.ownerGroupUuid = ownerUuid;
if (input.members != null && !input.members.isEmpty()) {
List<Account.Id> members = new ArrayList<>();
for (String nameOrEmailOrId : input.members) {
@@ -158,7 +158,7 @@
args.initialMembers = members;
} else {
args.initialMembers =
- ownerId == null
+ ownerUuid == null
? Collections.singleton(self.get().getAccountId())
: Collections.<Account.Id>emptySet();
}
@@ -174,10 +174,10 @@
return json.format(new InternalGroupDescription(createGroup(args)));
}
- private AccountGroup.Id owner(GroupInput input) throws UnprocessableEntityException {
+ private AccountGroup.UUID owner(GroupInput input) throws UnprocessableEntityException {
if (input.ownerId != null) {
GroupDescription.Internal d = groups.parseInternal(Url.decode(input.ownerId));
- return d.getId();
+ return d.getGroupUUID();
}
return null;
}
@@ -212,8 +212,8 @@
.build();
InternalGroupUpdate.Builder groupUpdateBuilder =
InternalGroupUpdate.builder().setVisibleToAll(createGroupArgs.visibleToAll);
- if (createGroupArgs.ownerGroupId != null) {
- Optional<InternalGroup> ownerGroup = groupCache.get(createGroupArgs.ownerGroupId);
+ if (createGroupArgs.ownerGroupUuid != null) {
+ Optional<InternalGroup> ownerGroup = groupCache.get(createGroupArgs.ownerGroupUuid);
ownerGroup.map(InternalGroup::getGroupUUID).ifPresent(groupUpdateBuilder::setOwnerGroupUUID);
}
if (createGroupArgs.groupDescription != null) {
diff --git a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
index b7a8d55..38b22a9 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupsCollection.java
@@ -17,7 +17,6 @@
import com.google.common.collect.ListMultimap;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AcceptsCreate;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -34,10 +33,14 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.group.InternalGroupDescription;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import java.util.Optional;
public class GroupsCollection
implements RestCollection<TopLevelResource, GroupResource>,
@@ -49,6 +52,7 @@
private final CreateGroup.Factory createGroup;
private final GroupControl.Factory groupControlFactory;
private final GroupBackend groupBackend;
+ private final GroupCache groupCache;
private final Provider<CurrentUser> self;
private boolean hasQuery2;
@@ -61,6 +65,7 @@
CreateGroup.Factory createGroup,
GroupControl.Factory groupControlFactory,
GroupBackend groupBackend,
+ GroupCache groupCache,
Provider<CurrentUser> self) {
this.views = views;
this.list = list;
@@ -68,6 +73,7 @@
this.createGroup = createGroup;
this.groupControlFactory = groupControlFactory;
this.groupBackend = groupBackend;
+ this.groupCache = groupCache;
this.self = self;
}
@@ -167,12 +173,15 @@
}
}
- // Might be a legacy AccountGroup.Id.
+ // Might be a numeric AccountGroup.Id. -> Internal group.
if (id.matches("^[1-9][0-9]*$")) {
try {
- AccountGroup.Id legacyId = AccountGroup.Id.parse(id);
- return groupControlFactory.controlFor(legacyId).getGroup();
- } catch (IllegalArgumentException | NoSuchGroupException e) {
+ AccountGroup.Id groupId = AccountGroup.Id.parse(id);
+ Optional<InternalGroup> group = groupCache.get(groupId);
+ if (group.isPresent()) {
+ return new InternalGroupDescription(group.get());
+ }
+ } catch (IllegalArgumentException e) {
// Ignored
}
}
diff --git a/java/com/google/gerrit/server/restapi/group/Index.java b/java/com/google/gerrit/server/restapi/group/Index.java
index 22003c0..1267f7a 100644
--- a/java/com/google/gerrit/server/restapi/group/Index.java
+++ b/java/com/google/gerrit/server/restapi/group/Index.java
@@ -20,22 +20,21 @@
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.group.GroupResource;
-import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.index.group.GroupIndexer;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.Optional;
@Singleton
public class Index implements RestModifyView<GroupResource, Input> {
- private final GroupCache groupCache;
+ private final Provider<GroupIndexer> indexer;
@Inject
- Index(GroupCache groupCache) {
- this.groupCache = groupCache;
+ Index(Provider<GroupIndexer> indexer) {
+ this.indexer = indexer;
}
@Override
@@ -51,11 +50,7 @@
String.format("External Group Not Allowed: %s", groupUuid.get()));
}
- Optional<InternalGroup> group = groupCache.get(groupUuid);
- // evicting the group from the cache, reindexes the group
- if (group.isPresent()) {
- groupCache.evict(group.get().getGroupUUID(), group.get().getId(), group.get().getNameKey());
- }
+ indexer.get().index(groupUuid);
return Response.none();
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/GetAccess.java b/java/com/google/gerrit/server/restapi/project/GetAccess.java
index 9e431df..21d6013 100644
--- a/java/com/google/gerrit/server/restapi/project/GetAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/GetAccess.java
@@ -24,11 +24,11 @@
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.Iterables;
import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.RefConfigSection;
import com.google.gerrit.common.data.WebLinkInfoCommon;
-import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.api.access.AccessSectionInfo;
import com.google.gerrit.extensions.api.access.PermissionInfo;
import com.google.gerrit.extensions.api.access.PermissionRuleInfo;
@@ -45,7 +45,6 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.WebLinks;
import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
@@ -58,8 +57,6 @@
import com.google.gerrit.server.project.ProjectJson;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.restapi.group.GroupJson;
-import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -77,9 +74,6 @@
public class GetAccess implements RestReadView<ProjectResource> {
private static final Logger LOG = LoggerFactory.getLogger(GetAccess.class);
- /** Marker value used in {@code Map<?, GroupInfo>} for groups not visible to current user. */
- private static final GroupInfo INVISIBLE_SENTINEL = new GroupInfo();
-
public static final ImmutableBiMap<PermissionRule.Action, PermissionRuleInfo.Action> ACTION_TYPE =
ImmutableBiMap.of(
PermissionRule.Action.ALLOW,
@@ -95,42 +89,36 @@
private final Provider<CurrentUser> user;
private final PermissionBackend permissionBackend;
- private final GroupControl.Factory groupControlFactory;
private final AllProjectsName allProjectsName;
private final ProjectJson projectJson;
private final ProjectCache projectCache;
private final MetaDataUpdate.Server metaDataUpdateFactory;
private final GroupBackend groupBackend;
- private final GroupJson groupJson;
private final WebLinks webLinks;
@Inject
public GetAccess(
Provider<CurrentUser> self,
PermissionBackend permissionBackend,
- GroupControl.Factory groupControlFactory,
AllProjectsName allProjectsName,
ProjectCache projectCache,
MetaDataUpdate.Server metaDataUpdateFactory,
ProjectJson projectJson,
GroupBackend groupBackend,
- GroupJson groupJson,
WebLinks webLinks) {
this.user = self;
this.permissionBackend = permissionBackend;
- this.groupControlFactory = groupControlFactory;
this.allProjectsName = allProjectsName;
this.projectJson = projectJson;
this.projectCache = projectCache;
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.groupBackend = groupBackend;
- this.groupJson = groupJson;
this.webLinks = webLinks;
}
public ProjectAccessInfo apply(Project.NameKey nameKey)
throws ResourceNotFoundException, ResourceConflictException, IOException,
- PermissionBackendException, OrmException {
+ PermissionBackendException {
ProjectState state = projectCache.checkedGet(nameKey);
if (state == null) {
throw new ResourceNotFoundException(nameKey.get());
@@ -141,7 +129,7 @@
@Override
public ProjectAccessInfo apply(ProjectResource rsrc)
throws ResourceNotFoundException, ResourceConflictException, IOException,
- PermissionBackendException, OrmException {
+ PermissionBackendException {
// Load the current configuration from the repository, ensuring it's the most
// recent version available. If it differs from what was in the project
// state, force a cache flush now.
@@ -189,7 +177,7 @@
info.local = new HashMap<>();
info.ownerOf = new HashSet<>();
- Map<AccountGroup.UUID, GroupInfo> visibleGroups = new HashMap<>();
+ Map<AccountGroup.UUID, GroupInfo> groups = new HashMap<>();
boolean canReadConfig = check(perm, RefNames.REFS_CONFIG, READ);
boolean canWriteConfig = check(perm, ProjectPermission.WRITE_CONFIG);
@@ -204,20 +192,20 @@
String name = section.getName();
if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
if (canWriteConfig) {
- info.local.put(name, createAccessSection(visibleGroups, section));
+ info.local.put(name, createAccessSection(groups, section));
info.ownerOf.add(name);
} else if (canReadConfig) {
- info.local.put(section.getName(), createAccessSection(visibleGroups, section));
+ info.local.put(section.getName(), createAccessSection(groups, section));
}
} else if (RefConfigSection.isValid(name)) {
if (check(perm, name, WRITE_CONFIG)) {
- info.local.put(name, createAccessSection(visibleGroups, section));
+ info.local.put(name, createAccessSection(groups, section));
info.ownerOf.add(name);
} else if (canReadConfig) {
- info.local.put(name, createAccessSection(visibleGroups, section));
+ info.local.put(name, createAccessSection(groups, section));
} else if (check(perm, name, READ)) {
// Filter the section to only add rules describing groups that
@@ -235,18 +223,15 @@
continue;
}
- GroupInfo group = loadGroup(visibleGroups, groupId);
-
- if (group != INVISIBLE_SENTINEL) {
- if (dstPerm == null) {
- if (dst == null) {
- dst = new AccessSection(name);
- info.local.put(name, createAccessSection(visibleGroups, dst));
- }
- dstPerm = dst.getPermission(srcPerm.getName(), true);
+ loadGroup(groups, groupId);
+ if (dstPerm == null) {
+ if (dst == null) {
+ dst = new AccessSection(name);
+ info.local.put(name, createAccessSection(groups, dst));
}
- dstPerm.add(srcRule);
+ dstPerm = dst.getPermission(srcPerm.getName(), true);
}
+ dstPerm.add(srcRule);
}
}
}
@@ -285,34 +270,31 @@
info.configVisible = canReadConfig || canWriteConfig;
info.groups =
- visibleGroups
+ groups
.entrySet()
.stream()
- .filter(e -> e.getValue() != INVISIBLE_SENTINEL)
+ .filter(e -> e.getValue() != null)
.collect(toMap(e -> e.getKey().get(), e -> e.getValue()));
return info;
}
- private GroupInfo loadGroup(Map<AccountGroup.UUID, GroupInfo> visibleGroups, AccountGroup.UUID id)
- throws OrmException {
- GroupInfo group = visibleGroups.get(id);
- if (group == null) {
- try {
- GroupControl control = groupControlFactory.controlFor(id);
- group = INVISIBLE_SENTINEL;
- if (control.isVisible()) {
- group = groupJson.format(control.getGroup());
- group.id = null;
- }
- } catch (NoSuchGroupException e) {
- LOG.warn("NoSuchGroupException; ignoring group " + id, e);
- group = INVISIBLE_SENTINEL;
+ private void loadGroup(Map<AccountGroup.UUID, GroupInfo> groups, AccountGroup.UUID id) {
+ if (!groups.containsKey(id)) {
+ GroupDescription.Basic basic = groupBackend.get(id);
+ GroupInfo group;
+ if (basic != null) {
+ group = new GroupInfo();
+ // The UI only needs name + URL, so don't populate other fields to avoid leaking data
+ // about groups invisible to the user.
+ group.name = basic.getName();
+ group.url = basic.getUrl();
+ } else {
+ LOG.warn("no such group: " + id);
+ group = null;
}
- visibleGroups.put(id, group);
+ groups.put(id, group);
}
-
- return group;
}
private static boolean check(PermissionBackend.ForProject ctx, String ref, RefPermission perm)
@@ -336,7 +318,7 @@
}
private AccessSectionInfo createAccessSection(
- Map<AccountGroup.UUID, GroupInfo> groups, AccessSection section) throws OrmException {
+ Map<AccountGroup.UUID, GroupInfo> groups, AccessSection section) {
AccessSectionInfo accessSectionInfo = new AccessSectionInfo();
accessSectionInfo.permissions = new HashMap<>();
for (Permission p : section.getPermissions()) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index a038bfe..b0148c1 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -27,8 +27,9 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.WebLinks;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.ProjectState;
@@ -58,7 +59,6 @@
private final GitRepositoryManager repoManager;
private final PermissionBackend permissionBackend;
private final Provider<CurrentUser> user;
- private final VisibleRefFilter.Factory refFilterFactory;
private final WebLinks links;
@Option(
@@ -111,12 +111,10 @@
GitRepositoryManager repoManager,
PermissionBackend permissionBackend,
Provider<CurrentUser> user,
- VisibleRefFilter.Factory refFilterFactory,
WebLinks webLinks) {
this.repoManager = repoManager;
this.permissionBackend = permissionBackend;
this.user = user;
- this.refFilterFactory = refFilterFactory;
this.links = webLinks;
}
@@ -130,7 +128,7 @@
@Override
public List<TagInfo> apply(ProjectResource resource)
- throws IOException, ResourceNotFoundException, RestApiException {
+ throws IOException, ResourceNotFoundException, RestApiException, PermissionBackendException {
resource.getProjectState().checkStatePermitsRead();
List<TagInfo> tags = new ArrayList<>();
@@ -139,8 +137,7 @@
try (Repository repo = getRepository(resource.getNameKey());
RevWalk rw = new RevWalk(repo)) {
Map<String, Ref> all =
- visibleTags(
- resource.getProjectState(), repo, repo.getRefDatabase().getRefs(Constants.R_TAGS));
+ visibleTags(resource.getNameKey(), repo, repo.getRefDatabase().getRefs(Constants.R_TAGS));
for (Ref ref : all.values()) {
tags.add(
createTagInfo(perm.ref(ref.getName()), ref, rw, resource.getProjectState(), links));
@@ -165,7 +162,7 @@
}
public TagInfo get(ProjectResource resource, IdString id)
- throws ResourceNotFoundException, IOException {
+ throws ResourceNotFoundException, IOException, PermissionBackendException {
try (Repository repo = getRepository(resource.getNameKey());
RevWalk rw = new RevWalk(repo)) {
String tagName = id.get();
@@ -174,7 +171,7 @@
}
Ref ref = repo.getRefDatabase().exactRef(tagName);
if (ref != null
- && !visibleTags(resource.getProjectState(), repo, ImmutableMap.of(ref.getName(), ref))
+ && !visibleTags(resource.getNameKey(), repo, ImmutableMap.of(ref.getName(), ref))
.isEmpty()) {
return createTagInfo(
permissionBackend
@@ -235,7 +232,15 @@
}
}
- private Map<String, Ref> visibleTags(ProjectState state, Repository repo, Map<String, Ref> tags) {
- return refFilterFactory.create(state, repo).setShowMetadata(false).filter(tags, true);
+ private Map<String, Ref> visibleTags(
+ Project.NameKey project, Repository repo, Map<String, Ref> tags)
+ throws PermissionBackendException {
+ return permissionBackend
+ .user(user)
+ .project(project)
+ .filter(
+ tags,
+ repo,
+ RefFilterOptions.builder().setFilterMeta(true).setFilterTagsSeparately(true).build());
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/TagsCollection.java b/java/com/google/gerrit/server/restapi/project/TagsCollection.java
index c503094..fdace77 100644
--- a/java/com/google/gerrit/server/restapi/project/TagsCollection.java
+++ b/java/com/google/gerrit/server/restapi/project/TagsCollection.java
@@ -21,6 +21,7 @@
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectResource;
import com.google.gerrit.server.project.TagResource;
import com.google.inject.Inject;
@@ -52,7 +53,7 @@
@Override
public TagResource parse(ProjectResource parent, IdString id)
- throws RestApiException, IOException {
+ throws RestApiException, IOException, PermissionBackendException {
parent.getProjectState().checkStatePermitsRead();
return new TagResource(parent.getProjectState(), parent.getUser(), list.get().get(parent, id));
}
diff --git a/java/com/google/gerrit/server/rules/StoredValues.java b/java/com/google/gerrit/server/rules/StoredValues.java
index 287845d..9fc3557 100644
--- a/java/com/google/gerrit/server/rules/StoredValues.java
+++ b/java/com/google/gerrit/server/rules/StoredValues.java
@@ -24,7 +24,6 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.AnonymousUser;
-import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.Accounts;
@@ -54,7 +53,6 @@
public static final StoredValue<Emails> EMAILS = create(Emails.class);
public static final StoredValue<ReviewDb> REVIEW_DB = create(ReviewDb.class);
public static final StoredValue<ChangeData> CHANGE_DATA = create(ChangeData.class);
- public static final StoredValue<CurrentUser> CURRENT_USER = create(CurrentUser.class);
public static final StoredValue<ProjectState> PROJECT_STATE = create(ProjectState.class);
public static Change getChange(Prolog engine) throws SystemException {
diff --git a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
index c274e56..43f39b2 100644
--- a/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/JdbcAccountPatchReviewStore.java
@@ -181,7 +181,7 @@
}
}
- private static void doCreateTable(Statement stmt) throws SQLException {
+ protected void doCreateTable(Statement stmt) throws SQLException {
stmt.executeUpdate(
"CREATE TABLE IF NOT EXISTS account_patch_reviews ("
+ "account_id INTEGER DEFAULT 0 NOT NULL, "
diff --git a/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java b/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
index af84465..d648ed0 100644
--- a/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
+++ b/java/com/google/gerrit/server/schema/MysqlAccountPatchReviewStore.java
@@ -22,6 +22,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.sql.SQLException;
+import java.sql.Statement;
import org.eclipse.jgit.lib.Config;
@Singleton
@@ -50,4 +51,17 @@
return new OrmException(op + " failure on ACCOUNT_PATCH_REVIEWS", err);
}
}
+
+ @Override
+ protected void doCreateTable(Statement stmt) throws SQLException {
+ stmt.executeUpdate(
+ "CREATE TABLE IF NOT EXISTS account_patch_reviews ("
+ + "account_id INTEGER DEFAULT 0 NOT NULL, "
+ + "change_id INTEGER DEFAULT 0 NOT NULL, "
+ + "patch_set_id INTEGER DEFAULT 0 NOT NULL, "
+ + "file_name VARCHAR(255) DEFAULT '' NOT NULL, "
+ + "CONSTRAINT primary_key_account_patch_reviews "
+ + "PRIMARY KEY (change_id, patch_set_id, account_id, file_name)"
+ + ")");
+ }
}
diff --git a/java/com/google/gerrit/server/schema/Schema_159.java b/java/com/google/gerrit/server/schema/Schema_159.java
index 3bb0e98..f97453e 100644
--- a/java/com/google/gerrit/server/schema/Schema_159.java
+++ b/java/com/google/gerrit/server/schema/Schema_159.java
@@ -35,26 +35,32 @@
@Override
protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
- DraftWorkflowMigrationStrategy strategy = DraftWorkflowMigrationStrategy.PRIVATE;
- if (ui.yesno(
- false, "Migrate draft changes to work-in-progress changes (default is private)?")) {
- strategy = DraftWorkflowMigrationStrategy.WORK_IN_PROGRESS;
+ DraftWorkflowMigrationStrategy strategy = DraftWorkflowMigrationStrategy.WORK_IN_PROGRESS;
+ if (ui.yesno(false, "Migrate draft changes to private changes (default is work-in-progress)")) {
+ strategy = DraftWorkflowMigrationStrategy.PRIVATE;
}
ui.message(
String.format("Replace draft changes with %s changes ...", strategy.name().toLowerCase()));
try (StatementExecutor e = newExecutor(db)) {
String column =
strategy == DraftWorkflowMigrationStrategy.PRIVATE ? "is_private" : "work_in_progress";
- // Mark changes private/wip if changes have status draft or
- // if they have any draft patch sets.
+ // Mark changes private/WIP and NEW if either:
+ // * they have status DRAFT
+ // * they have status NEW and have any draft patch sets
e.execute(
String.format(
- "UPDATE changes SET %s = 'Y', created_on = created_on WHERE status = 'd' OR "
- + "EXISTS (SELECT * FROM patch_sets WHERE "
- + "patch_sets.change_id = changes.change_id AND patch_sets.draft = 'Y')",
+ "UPDATE changes "
+ + "SET %s = 'Y', "
+ + " status = 'n', "
+ + " created_on = created_on "
+ + "WHERE status = 'd' "
+ + " OR (status = 'n' "
+ + " AND EXISTS "
+ + " (SELECT * "
+ + " FROM patch_sets "
+ + " WHERE patch_sets.change_id = changes.change_id "
+ + " AND patch_sets.draft = 'Y')) ",
column));
- // Change change status from draft to new.
- e.execute("UPDATE changes SET status = 'n', created_on = created_on WHERE status = 'd'");
}
ui.message("done");
}
diff --git a/java/com/google/gerrit/sshd/BaseCommand.java b/java/com/google/gerrit/sshd/BaseCommand.java
index 1833f9c..3bf13d2 100644
--- a/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/java/com/google/gerrit/sshd/BaseCommand.java
@@ -16,6 +16,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.base.Joiner;
import com.google.common.util.concurrent.Atomics;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
@@ -48,10 +49,6 @@
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicReference;
@@ -74,8 +71,6 @@
static final int STATUS_NOT_FOUND = PRIVATE_STATUS | 2;
public static final int STATUS_NOT_ADMIN = PRIVATE_STATUS | 3;
- private static final String MASK = "***";
-
@Option(name = "--", usage = "end of options", handler = EndOfOptionsHandler.class)
private boolean endOfOptions;
@@ -99,8 +94,6 @@
@Inject private SshScope.Context context;
- @Inject private SshCommandSensitiveFieldsCache cache;
-
/** Commands declared by a plugin can be scoped by the plugin name. */
@Inject(optional = true)
@PluginName
@@ -119,9 +112,8 @@
/** Unparsed command line options. */
private String[] argv;
- private List<String> maskedArgv = new ArrayList<>();
-
- private Set<String> sensitiveParameters = new HashSet<>();
+ /** trimmed command line arguments. */
+ private String[] trimmedArgv;
public BaseCommand() {
task = Atomics.newReference();
@@ -168,20 +160,24 @@
this.argv = argv;
}
- public List<String> getMaskedArguments() {
- return maskedArgv;
- }
-
- public String getFormattedMaskedArguments(String delimiter) {
- return String.join(delimiter, maskedArgv);
- }
-
- public void setMaskedArguments(List<String> argv) {
- this.maskedArgv = argv;
- }
-
- public boolean isSensitiveParameter(String param) {
- return sensitiveParameters.contains(param);
+ /**
+ * Trim the argument if it is spanning multiple lines.
+ *
+ * @return the arguments where all the multiple-line fields are trimmed.
+ */
+ protected String[] getTrimmedArguments() {
+ if (trimmedArgv == null && argv != null) {
+ trimmedArgv = new String[argv.length];
+ for (int i = 0; i < argv.length; i++) {
+ String arg = argv[i];
+ int indexOfMultiLine = arg.indexOf("\n");
+ if (indexOfMultiLine > -1) {
+ arg = arg.substring(0, indexOfMultiLine).concat(" [trimmed]");
+ }
+ trimmedArgv[i] = arg;
+ }
+ }
+ return trimmedArgv;
}
@Override
@@ -354,7 +350,7 @@
m.append(")");
}
m.append(" during ");
- m.append(getFormattedMaskedArguments(" "));
+ m.append(context.getCommandLine());
log.error(m.toString(), e);
}
@@ -399,8 +395,11 @@
}
protected String getTaskDescription() {
- StringBuilder m = new StringBuilder();
- m.append(getFormattedMaskedArguments(" "));
+ StringBuilder m = new StringBuilder(commandName);
+ String[] ta = getTrimmedArguments();
+ if (ta != null) {
+ m.append(Joiner.on(" ").join(ta));
+ }
return m.toString();
}
@@ -416,49 +415,12 @@
return m.toString();
}
- private void maskSensitiveParameters() {
- if (argv == null) {
- return;
- }
- sensitiveParameters = cache.get(this.getClass());
- maskedArgv = new ArrayList<>();
- maskedArgv.add(commandName);
- boolean maskNext = false;
- for (int i = 0; i < argv.length; i++) {
- if (maskNext) {
- maskedArgv.add(MASK);
- maskNext = false;
- continue;
- }
- String arg = argv[i];
- String key = extractKey(arg);
- if (isSensitiveParameter(key)) {
- maskNext = arg.equals(key);
- // When arg != key then parameter contains '=' sign and we mask them right away.
- // Otherwise we mask the next parameter as indicated by maskNext.
- if (!maskNext) {
- arg = key + "=" + MASK;
- }
- }
- maskedArgv.add(arg);
- }
- }
-
- private String extractKey(String arg) {
- int eqPos = arg.indexOf('=');
- if (eqPos > 0) {
- return arg.substring(0, eqPos);
- }
- return arg;
- }
-
private final class TaskThunk implements CancelableRunnable, ProjectRunnable {
private final CommandRunnable thunk;
private final String taskName;
private Project.NameKey projectName;
private TaskThunk(CommandRunnable thunk) {
- maskSensitiveParameters();
this.thunk = thunk;
this.taskName = getTaskName();
}
diff --git a/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index d061535..0287ceb 100644
--- a/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -29,7 +29,6 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -160,7 +159,7 @@
} catch (Exception e) {
logger.warn(
"Cannot start command \""
- + cmd.getFormattedMaskedArguments(" ")
+ + ctx.getCommandLine()
+ "\" for user "
+ ctx.getSession().getUsername(),
e);
@@ -180,10 +179,6 @@
try {
cmd = dispatcher.get();
cmd.setArguments(argv);
- cmd.setMaskedArguments(
- argv.length > 0
- ? Arrays.asList(argv[0])
- : Arrays.asList(ctx.getCommandLine().split(" ")[0]));
cmd.setInputStream(in);
cmd.setOutputStream(out);
cmd.setErrorStream(err);
diff --git a/java/com/google/gerrit/sshd/DispatchCommand.java b/java/com/google/gerrit/sshd/DispatchCommand.java
index 62f80c9..3f2e258 100644
--- a/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -107,10 +107,6 @@
atomicCmd.set(cmd);
cmd.start(env);
- if (cmd instanceof BaseCommand) {
- setMaskedArguments(((BaseCommand) cmd).getMaskedArguments());
- }
-
} catch (UnloggedFailure e) {
String msg = e.getMessage();
if (!msg.endsWith("\n")) {
diff --git a/java/com/google/gerrit/sshd/SensitiveData.java b/java/com/google/gerrit/sshd/SensitiveData.java
deleted file mode 100644
index 1dd7896..0000000
--- a/java/com/google/gerrit/sshd/SensitiveData.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd;
-
-import static java.lang.annotation.ElementType.FIELD;
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-/**
- * Annotation tagged on a field of an ssh command to indicate the value must be hidden from logs.
- */
-@Target({FIELD})
-@Retention(RUNTIME)
-public @interface SensitiveData {}
diff --git a/java/com/google/gerrit/sshd/SshCommandSensitiveFieldsCache.java b/java/com/google/gerrit/sshd/SshCommandSensitiveFieldsCache.java
deleted file mode 100644
index 8c79299..0000000
--- a/java/com/google/gerrit/sshd/SshCommandSensitiveFieldsCache.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// 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;
-
-import java.util.Set;
-
-/** Keeps data about ssh commands' parameters that have extra secure annotation. */
-public interface SshCommandSensitiveFieldsCache {
- Set<String> get(Class<?> command);
-
- void evictAll();
-}
diff --git a/java/com/google/gerrit/sshd/SshCommandSensitiveFieldsCacheImpl.java b/java/com/google/gerrit/sshd/SshCommandSensitiveFieldsCacheImpl.java
deleted file mode 100644
index b593388..0000000
--- a/java/com/google/gerrit/sshd/SshCommandSensitiveFieldsCacheImpl.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// 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;
-
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.inject.Inject;
-import com.google.inject.Module;
-import com.google.inject.TypeLiteral;
-import com.google.inject.name.Named;
-import java.lang.reflect.Field;
-import java.util.HashSet;
-import java.util.Set;
-import org.kohsuke.args4j.Option;
-
-public class SshCommandSensitiveFieldsCacheImpl implements SshCommandSensitiveFieldsCache {
- private static final String CACHE_NAME = "sshd_sensitive_command_params";
- private final LoadingCache<Class<?>, Set<String>> sshdCommandsCache;
-
- static Module module() {
- return new CacheModule() {
- @Override
- protected void configure() {
- cache(CACHE_NAME, new TypeLiteral<Class<?>>() {}, new TypeLiteral<Set<String>>() {})
- .loader(Loader.class);
- bind(SshCommandSensitiveFieldsCache.class).to(SshCommandSensitiveFieldsCacheImpl.class);
- }
- };
- }
-
- @Inject
- SshCommandSensitiveFieldsCacheImpl(@Named(CACHE_NAME) LoadingCache<Class<?>, Set<String>> cache) {
- sshdCommandsCache = cache;
- }
-
- @Override
- public Set<String> get(Class<?> cmd) {
- return sshdCommandsCache.getUnchecked(cmd);
- }
-
- @Override
- public void evictAll() {
- sshdCommandsCache.invalidateAll();
- }
-
- static class Loader extends CacheLoader<Class<?>, Set<String>> {
-
- @Override
- public Set<String> load(Class<?> cmd) throws Exception {
- Set<String> datas = new HashSet<>();
- for (Field field : cmd.getDeclaredFields()) {
- if (field.isAnnotationPresent(SensitiveData.class)) {
- Option option = field.getAnnotation(Option.class);
- datas.add(option.name());
- for (String opt : option.aliases()) {
- datas.add(opt);
- }
- }
- }
- return datas;
- }
- }
-}
diff --git a/java/com/google/gerrit/sshd/SshLog.java b/java/com/google/gerrit/sshd/SshLog.java
index 035989a..6b43d78 100644
--- a/java/com/google/gerrit/sshd/SshLog.java
+++ b/java/com/google/gerrit/sshd/SshLog.java
@@ -14,6 +14,7 @@
package com.google.gerrit.sshd;
+import com.google.common.base.Joiner;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.gerrit.common.TimeUtil;
@@ -48,12 +49,9 @@
private static final String P_STATUS = "status";
private static final String P_AGENT = "agent";
- private static final String MASK = "***";
-
private final Provider<SshSession> session;
private final Provider<Context> context;
private final AsyncAppender async;
- private final boolean auditMask;
private final AuditService auditService;
@Inject
@@ -67,7 +65,6 @@
this.context = context;
this.auditService = auditService;
- auditMask = config.getBoolean("audit", "maskSensitiveData", false);
if (!config.getBoolean("sshd", "requestLog", true)) {
async = null;
return;
@@ -125,7 +122,8 @@
final Context ctx = context.get();
ctx.finished = TimeUtil.nowMs();
- String cmd = extractWhat(dcmd, true);
+ String cmd = extractWhat(dcmd);
+
final LoggingEvent event = log(cmd);
event.setProperty(P_WAIT, (ctx.started - ctx.created) + "ms");
event.setProperty(P_EXEC, (ctx.finished - ctx.started) + "ms");
@@ -157,11 +155,7 @@
if (async != null) {
async.append(event);
}
-
- if (!auditMask) {
- cmd = extractWhat(dcmd, false);
- }
- audit(ctx, status, cmd, extractParameters(dcmd));
+ audit(context.get(), status, dcmd);
}
private ListMultimap<String, ?> extractParameters(DispatchCommand dcmd) {
@@ -184,10 +178,7 @@
// --param=value
int eqPos = arg.indexOf('=');
if (arg.startsWith("--") && eqPos > 0) {
- String param = arg.substring(0, eqPos);
- String value =
- auditMask && dcmd.isSensitiveParameter(param) ? MASK : arg.substring(eqPos + 1);
- parms.put(param, value);
+ parms.put(arg.substring(0, eqPos), arg.substring(eqPos + 1));
continue;
}
// -p value or --param value
@@ -202,7 +193,7 @@
if (paramName == null) {
parms.put("$" + argPos++, arg);
} else {
- parms.put(paramName, auditMask && dcmd.isSensitiveParameter(paramName) ? MASK : arg);
+ parms.put(paramName, arg);
paramName = null;
}
}
@@ -266,6 +257,10 @@
audit(ctx, result, cmd, null);
}
+ void audit(Context ctx, Object result, DispatchCommand cmd) {
+ audit(ctx, result, extractWhat(cmd), extractParameters(cmd));
+ }
+
private void audit(Context ctx, Object result, String cmd, ListMultimap<String, ?> params) {
String sessionId;
CurrentUser currentUser;
@@ -283,19 +278,14 @@
auditService.dispatch(new SshAuditEvent(sessionId, currentUser, cmd, created, params, result));
}
- private String extractWhat(DispatchCommand dcmd, boolean hideSensitive) {
+ private String extractWhat(DispatchCommand dcmd) {
if (dcmd == null) {
return "Command was already destroyed";
}
- return hideSensitive ? dcmd.getFormattedMaskedArguments(".") : extractWhat(dcmd);
- }
-
- private String extractWhat(DispatchCommand dcmd) {
- String name = dcmd.getCommandName();
- StringBuilder commandName = new StringBuilder(name == null ? "" : name);
- String[] args = dcmd.getArguments();
- for (int i = 1; i < args.length; i++) {
- commandName.append(".").append(args[i]);
+ StringBuilder commandName = new StringBuilder(dcmd.getCommandName());
+ String[] trimmedArgs = dcmd.getTrimmedArguments();
+ if (trimmedArgs != null) {
+ commandName.append(Joiner.on(".").join(trimmedArgs));
}
return commandName.toString();
}
diff --git a/java/com/google/gerrit/sshd/SshModule.java b/java/com/google/gerrit/sshd/SshModule.java
index c672b00..4ad8ba3 100644
--- a/java/com/google/gerrit/sshd/SshModule.java
+++ b/java/com/google/gerrit/sshd/SshModule.java
@@ -17,6 +17,8 @@
import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
import static com.google.inject.Scopes.SINGLETON;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.DynamicOptions;
@@ -37,6 +39,7 @@
import com.google.inject.servlet.RequestScoped;
import java.net.SocketAddress;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import org.apache.sshd.server.CommandFactory;
@@ -65,7 +68,6 @@
configureRequestScope();
install(new AsyncReceiveCommits.Module());
configureAliases();
- install(SshCommandSensitiveFieldsCacheImpl.module());
bind(SshLog.class);
bind(SshInfo.class).to(SshDaemon.class).in(SINGLETON);
@@ -109,10 +111,10 @@
CommandName gerrit = Commands.named("gerrit");
for (Map.Entry<String, String> e : aliases.entrySet()) {
String name = e.getKey();
- String[] dest = e.getValue().split("[ \\t]+");
- CommandName cmd = Commands.named(dest[0]);
- for (int i = 1; i < dest.length; i++) {
- cmd = Commands.named(cmd, dest[i]);
+ List<String> dest = Splitter.on(CharMatcher.whitespace()).splitToList(e.getValue());
+ CommandName cmd = Commands.named(dest.get(0));
+ for (int i = 1; i < dest.size(); i++) {
+ cmd = Commands.named(cmd, dest.get(i));
}
bind(Commands.key(gerrit, name)).toProvider(new AliasCommandProvider(cmd));
}
diff --git a/java/com/google/gerrit/sshd/SshPluginStarterCallback.java b/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
index 047690c..9ae1814 100644
--- a/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
+++ b/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -33,16 +33,13 @@
private final DispatchCommandProvider root;
private final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
- private final SshCommandSensitiveFieldsCache cache;
@Inject
SshPluginStarterCallback(
@CommandName(Commands.ROOT) DispatchCommandProvider root,
- DynamicMap<DynamicOptions.DynamicBean> dynamicBeans,
- SshCommandSensitiveFieldsCache cache) {
+ DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
this.root = root;
this.dynamicBeans = dynamicBeans;
- this.cache = cache;
}
@Override
@@ -59,7 +56,6 @@
if (cmd != null) {
newPlugin.add(root.replace(Commands.named(newPlugin.getName()), cmd));
}
- cache.evictAll();
}
private Provider<Command> load(Plugin plugin) {
diff --git a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index 2051a00..fa4a573 100644
--- a/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -227,9 +227,9 @@
throws UnloggedFailure {
Map<String, Map<String, ConfigValue>> m = new HashMap<>();
for (String pluginConfigValue : pluginConfigValues) {
- String[] s = pluginConfigValue.split("=");
- String[] s2 = s[0].split("\\.");
- if (s.length != 2 || s2.length != 2) {
+ List<String> s = Splitter.on('=').splitToList(pluginConfigValue);
+ List<String> s2 = Splitter.on('.').splitToList(s.get(0));
+ if (s.size() != 2 || s2.size() != 2) {
throw die(
"Invalid plugin config value '"
+ pluginConfigValue
@@ -237,14 +237,14 @@
+ " or '<plugin-name>.<parameter-name>=<value1,value2,...>'");
}
ConfigValue value = new ConfigValue();
- String v = s[1];
+ String v = s.get(1);
if (v.contains(",")) {
- value.values = Lists.newArrayList(Splitter.on(",").split(v));
+ value.values = Splitter.on(",").splitToList(v);
} else {
value.value = v;
}
- String pluginName = s2[0];
- String paramName = s2[1];
+ String pluginName = s2.get(0);
+ String paramName = s2.get(1);
Map<String, ConfigValue> l = m.get(pluginName);
if (l == null) {
l = new HashMap<>();
diff --git a/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index e467cc4..b51e178 100644
--- a/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -24,7 +24,9 @@
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.util.ManualRequestContext;
import com.google.gerrit.server.util.OneOffRequestContext;
@@ -49,7 +51,7 @@
public class LsUserRefs extends SshCommand {
@Inject private AccountResolver accountResolver;
@Inject private OneOffRequestContext requestContext;
- @Inject private VisibleRefFilter.Factory refFilterFactory;
+ @Inject private PermissionBackend permissionBackend;
@Inject private GitRepositoryManager repoManager;
@Option(
@@ -92,16 +94,17 @@
ManualRequestContext ctx = requestContext.openAs(userAccount.getId())) {
try {
Map<String, Ref> refsMap =
- refFilterFactory
- .create(projectState, repo)
- .filter(repo.getRefDatabase().getRefs(ALL), false);
+ permissionBackend
+ .user(user)
+ .project(projectName)
+ .filter(repo.getRefDatabase().getRefs(ALL), repo, RefFilterOptions.defaults());
for (String ref : refsMap.keySet()) {
if (!onlyRefsHeads || ref.startsWith(RefNames.REFS_HEADS)) {
stdout.println(ref);
}
}
- } catch (IOException e) {
+ } catch (IOException | PermissionBackendException e) {
throw new Failure(1, "fatal: Error reading refs: '" + projectName, e);
}
} catch (RepositoryNotFoundException e) {
diff --git a/java/com/google/gerrit/sshd/commands/Receive.java b/java/com/google/gerrit/sshd/commands/Receive.java
index 0e6ac49..a455c90 100644
--- a/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/java/com/google/gerrit/sshd/commands/Receive.java
@@ -20,7 +20,7 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
import com.google.gerrit.server.git.receive.AsyncReceiveCommits;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.permissions.PermissionBackend;
@@ -134,14 +134,14 @@
msg.append(" AdvertiseRefsHook: ").append(rp.getAdvertiseRefsHook());
if (rp.getAdvertiseRefsHook() == AdvertiseRefsHook.DEFAULT) {
msg.append("DEFAULT");
- } else if (rp.getAdvertiseRefsHook() instanceof VisibleRefFilter) {
- msg.append("VisibleRefFilter");
+ } else if (rp.getAdvertiseRefsHook() instanceof DefaultAdvertiseRefsHook) {
+ msg.append("DefaultAdvertiseRefsHook");
} else {
msg.append(rp.getAdvertiseRefsHook().getClass());
}
msg.append("\n");
- if (rp.getAdvertiseRefsHook() instanceof VisibleRefFilter) {
+ if (rp.getAdvertiseRefsHook() instanceof DefaultAdvertiseRefsHook) {
Map<String, Ref> adv = rp.getAdvertisedRefs();
msg.append(" Visible references (").append(adv.size()).append("):\n");
for (Ref ref : adv.values()) {
diff --git a/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 1be32a8..1d764b9 100644
--- a/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -240,11 +240,6 @@
}
}
- @Override
- protected String getTaskDescription() {
- return "gerrit review";
- }
-
private void applyReview(PatchSet patchSet, ReviewInput review) throws RestApiException {
gApi.changes()
.id(patchSet.getId().getParentKey().get())
diff --git a/java/com/google/gerrit/sshd/commands/ShowConnections.java b/java/com/google/gerrit/sshd/commands/ShowConnections.java
index 51c3ef4..c5485ef 100644
--- a/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -175,7 +175,7 @@
}
private static String time(long now, long time) {
- if (time - now < 24 * 60 * 60 * 1000L) {
+ if (now - time < 24 * 60 * 60 * 1000L) {
return new SimpleDateFormat("HH:mm:ss").format(new Date(time));
}
return new SimpleDateFormat("MMM-dd").format(new Date(time));
diff --git a/java/com/google/gerrit/sshd/commands/Upload.java b/java/com/google/gerrit/sshd/commands/Upload.java
index 0d78279..24a6975 100644
--- a/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/java/com/google/gerrit/sshd/commands/Upload.java
@@ -17,12 +17,13 @@
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.server.git.DefaultAdvertiseRefsHook;
import com.google.gerrit.server.git.TransferConfig;
import com.google.gerrit.server.git.UploadPackInitializer;
-import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.git.validators.UploadValidationException;
import com.google.gerrit.server.git.validators.UploadValidators;
import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.RefFilterOptions;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.sshd.AbstractGitCommand;
@@ -39,7 +40,6 @@
/** Publishes Git repositories over SSH using the Git upload-pack protocol. */
final class Upload extends AbstractGitCommand {
@Inject private TransferConfig config;
- @Inject private VisibleRefFilter.Factory refFilterFactory;
@Inject private DynamicSet<PreUploadHook> preUploadHooks;
@Inject private DynamicSet<PostUploadHook> postUploadHooks;
@Inject private DynamicSet<UploadPackInitializer> uploadPackInitializers;
@@ -49,11 +49,11 @@
@Override
protected void runImpl() throws IOException, Failure {
+ PermissionBackend.ForProject perm =
+ permissionBackend.user(user).project(projectState.getNameKey());
try {
- permissionBackend
- .user(user)
- .project(projectState.getNameKey())
- .check(ProjectPermission.RUN_UPLOAD_PACK);
+
+ perm.check(ProjectPermission.RUN_UPLOAD_PACK);
} catch (AuthException e) {
throw new Failure(1, "fatal: upload-pack not permitted on this server");
} catch (PermissionBackendException e) {
@@ -61,7 +61,7 @@
}
final UploadPack up = new UploadPack(repo);
- up.setAdvertiseRefsHook(refFilterFactory.create(projectState, repo));
+ up.setAdvertiseRefsHook(new DefaultAdvertiseRefsHook(perm, RefFilterOptions.defaults()));
up.setPackConfig(config.getPackConfig());
up.setTimeout(config.getTimeout());
up.setPostUploadHook(PostUploadHookChain.newChain(Lists.newArrayList(postUploadHooks)));
diff --git a/java/com/google/gerrit/sshd/commands/UploadArchive.java b/java/com/google/gerrit/sshd/commands/UploadArchive.java
index acb2eca..9ac522f 100644
--- a/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -16,6 +16,7 @@
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.server.change.ArchiveFormat;
@@ -142,8 +143,7 @@
if (!s.startsWith(argCmd)) {
throw new Failure(1, "fatal: 'argument' token or flush expected, got " + s);
}
- String[] parts = s.substring(argCmd.length()).split("=", 2);
- for (String p : parts) {
+ for (String p : Splitter.on('=').limit(2).split(s.substring(argCmd.length()))) {
args.add(p);
}
}
diff --git a/java/com/google/gerrit/testing/FakeEmailSender.java b/java/com/google/gerrit/testing/FakeEmailSender.java
index 7aed684..19e85db 100644
--- a/java/com/google/gerrit/testing/FakeEmailSender.java
+++ b/java/com/google/gerrit/testing/FakeEmailSender.java
@@ -23,6 +23,7 @@
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.MailHeader;
import com.google.gerrit.server.mail.send.EmailHeader;
import com.google.gerrit.server.mail.send.EmailSender;
import com.google.inject.AbstractModule;
@@ -148,8 +149,8 @@
}
public List<Message> getMessages(String changeId, String type) {
- final String idFooter = "\nGerrit-Change-Id: " + changeId + "\n";
- final String typeFooter = "\nGerrit-MessageType: " + type + "\n";
+ final String idFooter = "\n" + MailHeader.CHANGE_ID.withDelimiter() + changeId + "\n";
+ final String typeFooter = "\n" + MailHeader.MESSAGE_TYPE.withDelimiter() + type + "\n";
return getMessages()
.stream()
.filter(in -> in.body().contains(idFooter) && in.body().contains(typeFooter))
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index f6f18bd..3bea0a8 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -281,9 +281,11 @@
private Module indexModule(String moduleClassName) {
try {
+ boolean slave = cfg.getBoolean("container", "slave", false);
Class<?> clazz = Class.forName(moduleClassName);
- Method m = clazz.getMethod("singleVersionWithExplicitVersions", Map.class, int.class);
- return (Module) m.invoke(null, getSingleSchemaVersions(), 0);
+ Method m =
+ clazz.getMethod("singleVersionWithExplicitVersions", Map.class, int.class, boolean.class);
+ return (Module) m.invoke(null, getSingleSchemaVersions(), 0, slave);
} catch (ClassNotFoundException
| SecurityException
| NoSuchMethodException
diff --git a/java/com/google/gerrit/testing/InMemoryTestEnvironment.java b/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
index bffbcd6..cebd139 100644
--- a/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
+++ b/java/com/google/gerrit/testing/InMemoryTestEnvironment.java
@@ -121,7 +121,9 @@
schemaCreator.create(underlyingDb);
}
db = schemaFactory.open();
- setApiUser(accountManager.authenticate(AuthRequest.forUser("user")).getAccountId());
+
+ // The first user is added to the "Administrators" group. See AccountManager#create().
+ setApiUser(accountManager.authenticate(AuthRequest.forUser("admin")).getAccountId());
// Inject target members after setting API user, so it can @Inject a ReviewDb if it wants.
injector.injectMembers(target);
diff --git a/java/gerrit/PRED_current_user_1.java b/java/gerrit/PRED_current_user_1.java
deleted file mode 100644
index c7d381d..0000000
--- a/java/gerrit/PRED_current_user_1.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (C) 2011 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 gerrit;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.AnonymousUser;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.PeerDaemonUser;
-import com.google.gerrit.server.rules.StoredValues;
-import com.googlecode.prolog_cafe.exceptions.EvaluationException;
-import com.googlecode.prolog_cafe.exceptions.PrologException;
-import com.googlecode.prolog_cafe.lang.IntegerTerm;
-import com.googlecode.prolog_cafe.lang.Operation;
-import com.googlecode.prolog_cafe.lang.Predicate;
-import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.StructureTerm;
-import com.googlecode.prolog_cafe.lang.SymbolTerm;
-import com.googlecode.prolog_cafe.lang.Term;
-
-public class PRED_current_user_1 extends Predicate.P1 {
- private static final SymbolTerm user = SymbolTerm.intern("user", 1);
- private static final SymbolTerm anonymous = SymbolTerm.intern("anonymous");
- private static final SymbolTerm peerDaemon = SymbolTerm.intern("peer_daemon");
-
- public PRED_current_user_1(Term a1, Operation n) {
- arg1 = a1;
- cont = n;
- }
-
- @Override
- public Operation exec(Prolog engine) throws PrologException {
- engine.setB0();
- Term a1 = arg1.dereference();
-
- CurrentUser curUser = StoredValues.CURRENT_USER.getOrNull(engine);
- if (curUser == null) {
- throw new EvaluationException("Current user not available in this rule type");
- }
- Term resultTerm;
-
- if (curUser.isIdentifiedUser()) {
- Account.Id id = curUser.getAccountId();
- resultTerm = new IntegerTerm(id.get());
- } else if (curUser instanceof AnonymousUser) {
- resultTerm = anonymous;
- } else if (curUser instanceof PeerDaemonUser) {
- resultTerm = peerDaemon;
- } else {
- throw new EvaluationException("Unknown user type");
- }
-
- if (!a1.unify(new StructureTerm(user, resultTerm), engine.trail)) {
- return engine.fail();
- }
- return cont;
- }
-}
diff --git a/java/gerrit/PRED_current_user_2.java b/java/gerrit/PRED_current_user_2.java
deleted file mode 100644
index 4815b9f..0000000
--- a/java/gerrit/PRED_current_user_2.java
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright (C) 2011 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 gerrit;
-
-import static com.googlecode.prolog_cafe.lang.SymbolTerm.intern;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.rules.PrologEnvironment;
-import com.google.gerrit.server.rules.StoredValues;
-import com.googlecode.prolog_cafe.exceptions.IllegalTypeException;
-import com.googlecode.prolog_cafe.exceptions.PInstantiationException;
-import com.googlecode.prolog_cafe.exceptions.PrologException;
-import com.googlecode.prolog_cafe.lang.IntegerTerm;
-import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
-import com.googlecode.prolog_cafe.lang.Operation;
-import com.googlecode.prolog_cafe.lang.Predicate;
-import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.StructureTerm;
-import com.googlecode.prolog_cafe.lang.SymbolTerm;
-import com.googlecode.prolog_cafe.lang.Term;
-import com.googlecode.prolog_cafe.lang.VariableTerm;
-import java.util.Map;
-
-/**
- * Loads a CurrentUser object for a user identity.
- *
- * <p>Values are cached in the hash {@code current_user}, avoiding recreation during a single
- * evaluation.
- *
- * <pre>
- * current_user(user(+AccountId), -CurrentUser).
- * </pre>
- */
-class PRED_current_user_2 extends Predicate.P2 {
- private static final SymbolTerm user = intern("user", 1);
- private static final SymbolTerm anonymous = intern("anonymous");
-
- PRED_current_user_2(Term a1, Term a2, Operation n) {
- arg1 = a1;
- arg2 = a2;
- cont = n;
- }
-
- @Override
- public Operation exec(Prolog engine) throws PrologException {
- engine.setB0();
- Term a1 = arg1.dereference();
- Term a2 = arg2.dereference();
-
- if (a1 instanceof VariableTerm) {
- throw new PInstantiationException(this, 1);
- }
-
- if (!a2.unify(createUser(engine, a1), engine.trail)) {
- return engine.fail();
- }
-
- return cont;
- }
-
- public Term createUser(Prolog engine, Term key) {
- if (!(key instanceof StructureTerm)
- || key.arity() != 1
- || !((StructureTerm) key).functor().equals(user)) {
- throw new IllegalTypeException(this, 1, "user(int)", key);
- }
-
- Term idTerm = key.arg(0);
- CurrentUser user;
- if (idTerm instanceof IntegerTerm) {
- Map<Account.Id, IdentifiedUser> cache = StoredValues.USERS.get(engine);
- Account.Id accountId = new Account.Id(((IntegerTerm) idTerm).intValue());
- user = cache.get(accountId);
- if (user == null) {
- IdentifiedUser.GenericFactory userFactory = userFactory(engine);
- IdentifiedUser who = userFactory.create(accountId);
- cache.put(accountId, who);
- user = who;
- }
-
- } else if (idTerm.equals(anonymous)) {
- user = StoredValues.ANONYMOUS_USER.get(engine);
-
- } else {
- throw new IllegalTypeException(this, 1, "user(int)", key);
- }
-
- return new JavaObjectTerm(user);
- }
-
- private static IdentifiedUser.GenericFactory userFactory(Prolog engine) {
- return ((PrologEnvironment) engine.control).getArgs().getUserFactory();
- }
-}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index de73551..9c89429 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -1558,9 +1558,20 @@
@Test
public void implicitlyCcOnNonVotingReviewPgStyle() throws Exception {
+ testImplicitlyCcOnNonVotingReviewPgStyle(user);
+ }
+
+ @Test
+ public void implicitlyCcOnNonVotingReviewForUserWithoutUserNamePgStyle() throws Exception {
+ TestAccount accountWithoutUsername = accountCreator.create();
+ assertThat(accountWithoutUsername.username).isNull();
+ testImplicitlyCcOnNonVotingReviewPgStyle(accountWithoutUsername);
+ }
+
+ private void testImplicitlyCcOnNonVotingReviewPgStyle(TestAccount testAccount) throws Exception {
PushOneCommit.Result r = createChange();
- setApiUser(user);
- assertThat(getReviewerState(r.getChangeId(), user.id)).isEmpty();
+ setApiUser(testAccount);
+ assertThat(getReviewerState(r.getChangeId(), testAccount.id)).isEmpty();
// Exact request format made by PG UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8.
ReviewInput in = new ReviewInput();
@@ -1571,15 +1582,26 @@
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
// If we're not reading from NoteDb, then the CCed user will be returned in the REVIEWER state.
- assertThat(getReviewerState(r.getChangeId(), user.id))
+ assertThat(getReviewerState(r.getChangeId(), testAccount.id))
.hasValue(notesMigration.readChanges() ? CC : REVIEWER);
}
@Test
public void implicitlyCcOnNonVotingReviewGwtStyle() throws Exception {
+ testImplicitlyCcOnNonVotingReviewGwtStyle(user);
+ }
+
+ @Test
+ public void implicitlyCcOnNonVotingReviewForUserWithoutUserNameGwtStyle() throws Exception {
+ TestAccount accountWithoutUsername = accountCreator.create();
+ assertThat(accountWithoutUsername.username).isNull();
+ testImplicitlyCcOnNonVotingReviewGwtStyle(accountWithoutUsername);
+ }
+
+ private void testImplicitlyCcOnNonVotingReviewGwtStyle(TestAccount testAccount) throws Exception {
PushOneCommit.Result r = createChange();
- setApiUser(user);
- assertThat(getReviewerState(r.getChangeId(), user.id)).isEmpty();
+ setApiUser(testAccount);
+ assertThat(getReviewerState(r.getChangeId(), testAccount.id)).isEmpty();
// Exact request format made by GWT UI at ddc6b7160fe416fed9e7e3180489d44c82fd64f8.
ReviewInput in = new ReviewInput();
@@ -1589,7 +1611,7 @@
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
// If we're not reading from NoteDb, then the CCed user will be returned in the REVIEWER state.
- assertThat(getReviewerState(r.getChangeId(), user.id))
+ assertThat(getReviewerState(r.getChangeId(), testAccount.id))
.hasValue(notesMigration.readChanges() ? CC : REVIEWER);
}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/BUILD b/javatests/com/google/gerrit/acceptance/api/group/BUILD
index 781122b..21294f5 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/BUILD
+++ b/javatests/com/google/gerrit/acceptance/api/group/BUILD
@@ -7,6 +7,8 @@
deps = [
":util",
"//java/com/google/gerrit/server/group/db/testing",
+ "//java/com/google/gerrit/server/group/testing",
+ "//java/com/google/gerrit/truth",
"//javatests/com/google/gerrit/acceptance/rest/account:util",
],
)
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
new file mode 100644
index 0000000..389efb4
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupIndexerIT.java
@@ -0,0 +1,191 @@
+// 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.group;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.server.notedb.NoteDbTable.GROUPS;
+import static com.google.gerrit.server.notedb.NotesMigration.DISABLE_REVIEW_DB;
+import static com.google.gerrit.server.notedb.NotesMigration.READ;
+import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
+import static com.google.gerrit.server.notedb.NotesMigration.WRITE;
+import static com.google.gerrit.truth.ListSubject.assertThat;
+import static com.google.gerrit.truth.OptionalSubject.assertThat;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.api.GerritApi;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.GroupCache;
+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.group.testing.InternalGroupSubject;
+import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.gerrit.server.query.group.InternalGroupQuery;
+import com.google.gerrit.testing.InMemoryTestEnvironment;
+import com.google.gerrit.truth.ListSubject;
+import com.google.gerrit.truth.OptionalSubject;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class GroupIndexerIT {
+ private static Config createPureNoteDbConfig() {
+ Config config = new Config();
+ config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), WRITE, true);
+ config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), READ, true);
+ config.setBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, true);
+ return config;
+ }
+
+ @Rule
+ public InMemoryTestEnvironment testEnvironment =
+ new InMemoryTestEnvironment(GroupIndexerIT::createPureNoteDbConfig);
+
+ @Inject private GroupIndexer groupIndexer;
+ @Inject private GerritApi gApi;
+ @Inject private GroupCache groupCache;
+ @Inject private ReviewDb db;
+ @Inject @ServerInitiated private GroupsUpdate groupsUpdate;
+ @Inject private Provider<InternalGroupQuery> groupQueryProvider;
+
+ @Test
+ public void indexingUpdatesTheIndex() throws Exception {
+ AccountGroup.UUID groupUuid = createGroup("users");
+ AccountGroup.UUID subgroupUuid = new AccountGroup.UUID("contributors");
+ updateGroupWithoutCacheOrIndex(
+ groupUuid,
+ newGroupUpdate()
+ .setSubgroupModification(subgroups -> ImmutableSet.of(subgroupUuid))
+ .build());
+
+ groupIndexer.index(groupUuid);
+
+ List<InternalGroup> parentGroups = groupQueryProvider.get().bySubgroup(subgroupUuid);
+ assertThatGroups(parentGroups).onlyElement().groupUuid().isEqualTo(groupUuid);
+ }
+
+ @Test
+ public void indexCannotBeCorruptedByStaleCache() throws Exception {
+ AccountGroup.UUID groupUuid = createGroup("verifiers");
+ loadGroupToCache(groupUuid);
+ AccountGroup.UUID subgroupUuid = new AccountGroup.UUID("contributors");
+ updateGroupWithoutCacheOrIndex(
+ groupUuid,
+ newGroupUpdate()
+ .setSubgroupModification(subgroups -> ImmutableSet.of(subgroupUuid))
+ .build());
+
+ groupIndexer.index(groupUuid);
+
+ List<InternalGroup> parentGroups = groupQueryProvider.get().bySubgroup(subgroupUuid);
+ assertThatGroups(parentGroups).onlyElement().groupUuid().isEqualTo(groupUuid);
+ }
+
+ @Test
+ public void indexingUpdatesStaleUuidCache() throws Exception {
+ AccountGroup.UUID groupUuid = createGroup("verifiers");
+ loadGroupToCache(groupUuid);
+ updateGroupWithoutCacheOrIndex(groupUuid, newGroupUpdate().setDescription("Modified").build());
+
+ groupIndexer.index(groupUuid);
+
+ Optional<InternalGroup> updatedGroup = groupCache.get(groupUuid);
+ assertThatGroup(updatedGroup).value().description().isEqualTo("Modified");
+ }
+
+ @Test
+ public void reindexingStaleGroupUpdatesTheIndex() throws Exception {
+ AccountGroup.UUID groupUuid = createGroup("users");
+ AccountGroup.UUID subgroupUuid = new AccountGroup.UUID("contributors");
+ updateGroupWithoutCacheOrIndex(
+ groupUuid,
+ newGroupUpdate()
+ .setSubgroupModification(subgroups -> ImmutableSet.of(subgroupUuid))
+ .build());
+
+ groupIndexer.reindexIfStale(groupUuid);
+
+ List<InternalGroup> parentGroups = groupQueryProvider.get().bySubgroup(subgroupUuid);
+ assertThatGroups(parentGroups).onlyElement().groupUuid().isEqualTo(groupUuid);
+ }
+
+ @Test
+ public void notStaleGroupIsNotReindexed() throws Exception {
+ AccountGroup.UUID groupUuid = createGroup("verifiers");
+ updateGroupWithoutCacheOrIndex(groupUuid, newGroupUpdate().setDescription("Modified").build());
+ groupIndexer.index(groupUuid);
+
+ boolean reindexed = groupIndexer.reindexIfStale(groupUuid);
+
+ assertWithMessage("Group should not have been reindexed").that(reindexed).isFalse();
+ }
+
+ @Test
+ public void indexStalenessIsNotDerivedFromCacheStaleness() throws Exception {
+ AccountGroup.UUID groupUuid = createGroup("verifiers");
+ updateGroupWithoutCacheOrIndex(groupUuid, newGroupUpdate().setDescription("Modified").build());
+ reloadGroupToCache(groupUuid);
+
+ boolean reindexed = groupIndexer.reindexIfStale(groupUuid);
+
+ assertWithMessage("Group should have been reindexed").that(reindexed).isTrue();
+ }
+
+ private AccountGroup.UUID createGroup(String name) throws RestApiException {
+ GroupInfo group = gApi.groups().create(name).get();
+ return new AccountGroup.UUID(group.id);
+ }
+
+ private void reloadGroupToCache(AccountGroup.UUID groupUuid) {
+ groupCache.evict(groupUuid);
+ loadGroupToCache(groupUuid);
+ }
+
+ private void loadGroupToCache(AccountGroup.UUID groupUuid) {
+ groupCache.get(groupUuid);
+ }
+
+ private static InternalGroupUpdate.Builder newGroupUpdate() {
+ return InternalGroupUpdate.builder();
+ }
+
+ private void updateGroupWithoutCacheOrIndex(
+ AccountGroup.UUID groupUuid, InternalGroupUpdate groupUpdate)
+ throws OrmException, NoSuchGroupException, IOException, ConfigInvalidException {
+ groupsUpdate.updateGroupInDb(db, groupUuid, groupUpdate);
+ }
+
+ private static OptionalSubject<InternalGroupSubject, InternalGroup> assertThatGroup(
+ Optional<InternalGroup> updatedGroup) {
+ return assertThat(updatedGroup, InternalGroupSubject::assertThat);
+ }
+
+ private static ListSubject<InternalGroupSubject, InternalGroup> assertThatGroups(
+ List<InternalGroup> parentGroups) {
+ return assertThat(parentGroups, InternalGroupSubject::assertThat);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 39d927e..2d5703d 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -33,9 +33,9 @@
import static java.util.stream.Collectors.toList;
import com.google.common.base.Throwables;
-import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.AtomicLongMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.NoHttpd;
@@ -60,6 +60,9 @@
import com.google.gerrit.extensions.common.GroupAuditEventInfo.UserMemberAuditEventInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.GroupOptionsInfo;
+import com.google.gerrit.extensions.events.GroupIndexedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -71,11 +74,17 @@
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.Sequences;
+import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.account.GroupIncludeCache;
import com.google.gerrit.server.group.InternalGroup;
+import com.google.gerrit.server.group.PeriodicGroupIndexer;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.group.db.Groups;
import com.google.gerrit.server.group.db.GroupsConsistencyChecker;
+import com.google.gerrit.server.group.db.GroupsUpdate;
+import com.google.gerrit.server.group.db.InternalGroupCreation;
+import com.google.gerrit.server.group.db.InternalGroupUpdate;
import com.google.gerrit.server.index.group.GroupIndexer;
import com.google.gerrit.server.index.group.StalenessChecker;
import com.google.gerrit.server.util.MagicBranch;
@@ -83,7 +92,6 @@
import com.google.gerrit.testing.TestTimeUtil;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import com.google.inject.name.Named;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@@ -94,7 +102,6 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
@@ -132,14 +139,14 @@
}
@Inject private Groups groups;
+ @Inject @ServerInitiated private GroupsUpdate groupsUpdate;
@Inject private GroupIncludeCache groupIncludeCache;
@Inject private StalenessChecker stalenessChecker;
@Inject private GroupIndexer groupIndexer;
@Inject private GroupsConsistencyChecker consistencyChecker;
-
- @Inject
- @Named("groups_byuuid")
- private LoadingCache<String, Optional<InternalGroup>> groupsByUUIDCache;
+ @Inject private PeriodicGroupIndexer slaveGroupIndexer;
+ @Inject private DynamicSet<GroupIndexedListener> groupIndexedListeners;
+ @Inject private Sequences seq;
@Before
public void setTimeForTesting() {
@@ -1227,8 +1234,7 @@
@Test
@Sandboxed
public void groupsOfUserCanBeListedInSlaveMode() throws Exception {
- // TODO(aliceks): Remove this line when we have a group index in slave mode.
- assume().that(readGroupsFromNoteDb()).isFalse();
+ assume().that(readGroupsFromNoteDb()).isTrue();
GroupInput groupInput = new GroupInput();
groupInput.name = name("contributors");
@@ -1243,11 +1249,96 @@
assertThat(groupNames).contains(groupInput.name);
}
+ @Test
+ @Sandboxed
+ @GerritConfig(name = "index.scheduledIndexer.enabled", value = "false")
+ @GerritConfig(name = "index.autoReindexIfStale", value = "false")
+ @IgnoreGroupInconsistencies
+ public void reindexGroupsInSlaveMode() throws Exception {
+ assume().that(readGroupsFromNoteDb()).isTrue();
+ assume().that(cfg.getBoolean(SECTION_NOTE_DB, GROUPS.key(), DISABLE_REVIEW_DB, false)).isTrue();
+
+ List<AccountGroup.UUID> expectedGroups =
+ groups.getAllGroupReferences(db).map(GroupReference::getUUID).collect(toList());
+ assertThat(expectedGroups.size()).isAtLeast(2);
+
+ // Restart the server as slave, on startup of the slave all groups are indexed.
+ restartAsSlave();
+
+ GroupIndexedCounter groupIndexedCounter = new GroupIndexedCounter();
+ RegistrationHandle groupIndexEventCounterHandle =
+ groupIndexedListeners.add(groupIndexedCounter);
+ try {
+ // Running the reindexer right after startup should not need to reindex any group since
+ // reindexing was already done on startup.
+ slaveGroupIndexer.run();
+ groupIndexedCounter.assertNoReindex();
+
+ // Create a group without updating the cache or index,
+ // then run the reindexer -> only the new group is reindexed.
+ String groupName = "foo";
+ AccountGroup.UUID groupUuid = new AccountGroup.UUID(groupName + "-UUID");
+ groupsUpdate.createGroupInNoteDb(
+ InternalGroupCreation.builder()
+ .setGroupUUID(groupUuid)
+ .setNameKey(new AccountGroup.NameKey(groupName))
+ .setId(new AccountGroup.Id(seq.nextGroupId()))
+ .build(),
+ InternalGroupUpdate.builder().build());
+ slaveGroupIndexer.run();
+ groupIndexedCounter.assertReindexOf(groupUuid);
+
+ // Update a group without updating the cache or index,
+ // then run the reindexer -> only the updated group is reindexed.
+ groupsUpdate.updateGroupInDb(
+ db, groupUuid, InternalGroupUpdate.builder().setDescription("bar").build());
+ slaveGroupIndexer.run();
+ groupIndexedCounter.assertReindexOf(groupUuid);
+
+ // Delete a group without updating the cache or index,
+ // then run the reindexer -> only the deleted group is reindexed.
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ RefUpdate u = repo.updateRef(RefNames.refsGroups(groupUuid));
+ u.setForceUpdate(true);
+ assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED);
+ }
+ slaveGroupIndexer.run();
+ groupIndexedCounter.assertReindexOf(groupUuid);
+ } finally {
+ groupIndexEventCounterHandle.remove();
+ }
+ }
+
+ @Test
+ @Sandboxed
+ @GerritConfig(name = "index.scheduledIndexer.runOnStartup", value = "false")
+ @GerritConfig(name = "index.scheduledIndexer.enabled", value = "false")
+ @GerritConfig(name = "index.autoReindexIfStale", value = "false")
+ @IgnoreGroupInconsistencies
+ public void disabledReindexGroupsOnStartupSlaveMode() throws Exception {
+ assume().that(readGroupsFromNoteDb()).isTrue();
+
+ List<AccountGroup.UUID> expectedGroups =
+ groups.getAllGroupReferences(db).map(GroupReference::getUUID).collect(toList());
+ assertThat(expectedGroups.size()).isAtLeast(2);
+
+ restartAsSlave();
+
+ GroupIndexedCounter groupIndexedCounter = new GroupIndexedCounter();
+ RegistrationHandle groupIndexEventCounterHandle =
+ groupIndexedListeners.add(groupIndexedCounter);
+ try {
+ // No group indexing happened on startup. All groups should be reindexed now.
+ slaveGroupIndexer.run();
+ groupIndexedCounter.assertReindexOf(expectedGroups);
+ } finally {
+ groupIndexEventCounterHandle.remove();
+ }
+ }
+
private void assertStaleGroupAndReindex(AccountGroup.UUID groupUuid) throws IOException {
- // Evict group from cache to be sure that we use the index state for staleness checks. This has
- // to happen directly on the groupsByUUID cache because GroupsCacheImpl triggers a reindex for
- // the group.
- groupsByUUIDCache.invalidate(groupUuid.get());
+ // Evict group from cache to be sure that we use the index state for staleness checks.
+ groupCache.evict(groupUuid);
assertThat(stalenessChecker.isStale(groupUuid)).isTrue();
// Reindex fixes staleness
@@ -1402,4 +1493,38 @@
@Target({METHOD})
@Retention(RUNTIME)
private @interface IgnoreGroupInconsistencies {}
+
+ /** Checks if a group is indexed the correct number of times. */
+ private static class GroupIndexedCounter implements GroupIndexedListener {
+ private final AtomicLongMap<String> countsByGroup = AtomicLongMap.create();
+
+ @Override
+ public void onGroupIndexed(String uuid) {
+ countsByGroup.incrementAndGet(uuid);
+ }
+
+ void clear() {
+ countsByGroup.clear();
+ }
+
+ long getCount(AccountGroup.UUID groupUuid) {
+ return countsByGroup.get(groupUuid.get());
+ }
+
+ void assertReindexOf(AccountGroup.UUID groupUuid) {
+ assertReindexOf(ImmutableList.of(groupUuid));
+ }
+
+ void assertReindexOf(List<AccountGroup.UUID> groupUuids) {
+ for (AccountGroup.UUID groupUuid : groupUuids) {
+ assertThat(getCount(groupUuid)).named(groupUuid.get()).isEqualTo(1);
+ }
+ assertThat(countsByGroup).hasSize(groupUuids.size());
+ clear();
+ }
+
+ void assertNoReindex() {
+ assertThat(countsByGroup).isEmpty();
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index 0b2e5fb..4791d4c 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -98,47 +98,48 @@
@Test
public void nonexistentPermission() throws Exception {
- exception.expect(BadRequestException.class);
- exception.expectMessage("not recognized");
-
AccessCheckInput in = new AccessCheckInput();
in.account = user.email;
in.permission = "notapermission";
in.ref = "refs/heads/master";
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("not recognized");
gApi.projects().name(normalProject.get()).checkAccess(in);
}
@Test
public void permissionLacksRef() throws Exception {
- exception.expect(BadRequestException.class);
- exception.expectMessage("must set 'ref'");
AccessCheckInput in = new AccessCheckInput();
in.account = user.email;
in.permission = "forge_author";
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("must set 'ref'");
gApi.projects().name(normalProject.get()).checkAccess(in);
}
@Test
public void changePermission() throws Exception {
- exception.expect(BadRequestException.class);
- exception.expectMessage("recognized as ref permission");
-
AccessCheckInput in = new AccessCheckInput();
in.account = user.email;
in.permission = "rebase";
in.ref = "refs/heads/master";
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("recognized as ref permission");
gApi.projects().name(normalProject.get()).checkAccess(in);
}
@Test
public void nonexistentEmail() throws Exception {
- exception.expect(UnprocessableEntityException.class);
- exception.expectMessage("cannot find account doesnotexist@invalid.com");
-
AccessCheckInput in = new AccessCheckInput();
in.account = "doesnotexist@invalid.com";
in.permission = "rebase";
in.ref = "refs/heads/master";
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("cannot find account doesnotexist@invalid.com");
gApi.projects().name(normalProject.get()).checkAccess(in);
}
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 081eed3..e0c16e0f 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -48,10 +48,11 @@
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.git.receive.ReceiveCommitsAdvertiseRefsHook;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
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.testing.Util;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testing.NoteDbMode;
@@ -77,7 +78,7 @@
@NoHttpd
public class RefAdvertisementIT extends AbstractDaemonTest {
- @Inject private VisibleRefFilter.Factory refFilterFactory;
+ @Inject private PermissionBackend permissionBackend;
@Inject private ChangeNoteUtil noteUtil;
@Inject @AnonymousCowardName private String anonymousCowardName;
@Inject private AllUsersName allUsersName;
@@ -346,7 +347,7 @@
try (Repository repo = repoManager.openRepository(project)) {
assertRefs(
repo,
- refFilterFactory.create(projectCache.get(project), repo),
+ permissionBackend.user(user(user)).project(project),
// Can't use stored values from the index so DB must be enabled.
false,
"HEAD",
@@ -369,12 +370,10 @@
public void uploadPackSequencesWithAccessDatabase() throws Exception {
assume().that(notesMigration.readChangeSequence()).isTrue();
try (Repository repo = repoManager.openRepository(allProjects)) {
- setApiUser(user);
- assertRefs(repo, newFilter(repo, allProjects), true);
+ assertRefs(repo, newFilter(allProjects, user), true);
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
- setApiUser(user);
- assertRefs(repo, newFilter(repo, allProjects), true, "refs/sequences/changes");
+ assertRefs(repo, newFilter(allProjects, user), true, "refs/sequences/changes");
}
}
@@ -665,10 +664,13 @@
try (Repository repo = repoManager.openRepository(allUsers)) {
Map<String, Ref> all = repo.getAllRefs();
- VisibleRefFilter filter = refFilterFactory.create(projectCache.get(allUsers), repo);
- assertThat(filter.filter(all, false).keySet()).containsExactlyElementsIn(expectedAllRefs);
-
- assertThat(filter.setShowMetadata(false).filter(all, false).keySet())
+ PermissionBackend.ForProject forProject = newFilter(allUsers, admin);
+ assertThat(forProject.filter(all, repo, RefFilterOptions.defaults()).keySet())
+ .containsExactlyElementsIn(expectedAllRefs);
+ assertThat(
+ forProject
+ .filter(all, repo, RefFilterOptions.builder().setFilterMeta(true).build())
+ .keySet())
.containsExactlyElementsIn(expectedNonMetaRefs);
}
}
@@ -706,13 +708,15 @@
*/
private void assertUploadPackRefs(String... expectedWithMeta) throws Exception {
try (Repository repo = repoManager.openRepository(project)) {
- assertRefs(
- repo, refFilterFactory.create(projectCache.get(project), repo), true, expectedWithMeta);
+ assertRefs(repo, permissionBackend.user(user(user)).project(project), true, expectedWithMeta);
}
}
private void assertRefs(
- Repository repo, VisibleRefFilter filter, boolean disableDb, String... expectedWithMeta)
+ Repository repo,
+ PermissionBackend.ForProject forProject,
+ boolean disableDb,
+ String... expectedWithMeta)
throws Exception {
List<String> expected = new ArrayList<>(expectedWithMeta.length);
for (String r : expectedWithMeta) {
@@ -727,7 +731,8 @@
}
try {
Map<String, Ref> all = repo.getAllRefs();
- assertThat(filter.filter(all, false).keySet()).containsExactlyElementsIn(expected);
+ assertThat(forProject.filter(all, repo, RefFilterOptions.defaults()).keySet())
+ .containsExactlyElementsIn(expected);
} finally {
if (disableDb) {
enableDb(ctx);
@@ -743,8 +748,8 @@
}
}
- private VisibleRefFilter newFilter(Repository repo, Project.NameKey project) {
- return refFilterFactory.create(projectCache.get(project), repo);
+ private PermissionBackend.ForProject newFilter(Project.NameKey project, TestAccount u) {
+ return permissionBackend.user(user(u)).project(project);
}
private static ObjectId obj(ChangeData cd, int psNum) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
index f2a5d2f..4b6f8b2 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ReindexIT.java
@@ -16,6 +16,7 @@
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.extensions.client.ListGroupsOption.MEMBERS;
@@ -27,6 +28,7 @@
import com.google.gerrit.acceptance.pgm.IndexUpgradeController.UpgradeAttempt;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.index.GerritIndexStatus;
@@ -36,6 +38,8 @@
import com.google.inject.Provider;
import java.nio.file.Files;
import java.util.Set;
+import java.util.function.Consumer;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.util.FS;
import org.junit.Test;
@@ -80,6 +84,91 @@
}
@Test
+ public void offlineReindexForChangesIsNotPossibleInSlaveMode() throws Exception {
+ enableSlaveMode();
+
+ int exitCode =
+ runGerritAndReturnExitCode(
+ "reindex",
+ "--index",
+ "changes",
+ "-d",
+ sitePaths.site_path.toString(),
+ "--show-stack-trace");
+
+ assertWithMessage("Slave hosts shouldn't allow to offline reindex changes")
+ .that(exitCode)
+ .isGreaterThan(0);
+ }
+
+ @Test
+ public void offlineReindexForAccountsIsNotPossibleInSlaveMode() throws Exception {
+ enableSlaveMode();
+
+ int exitCode =
+ runGerritAndReturnExitCode(
+ "reindex",
+ "--index",
+ "accounts",
+ "-d",
+ sitePaths.site_path.toString(),
+ "--show-stack-trace");
+
+ assertWithMessage("Slave hosts shouldn't allow to offline reindex accounts")
+ .that(exitCode)
+ .isGreaterThan(0);
+ }
+
+ @Test
+ public void offlineReindexForProjectsIsNotPossibleInSlaveMode() throws Exception {
+ enableSlaveMode();
+
+ int exitCode =
+ runGerritAndReturnExitCode(
+ "reindex",
+ "--index",
+ "projects",
+ "-d",
+ sitePaths.site_path.toString(),
+ "--show-stack-trace");
+
+ assertWithMessage("Slave hosts shouldn't allow to offline reindex projects")
+ .that(exitCode)
+ .isGreaterThan(0);
+ }
+
+ @Test
+ public void offlineReindexForGroupsIsPossibleInSlaveMode() throws Exception {
+ enableSlaveMode();
+
+ int exitCode =
+ runGerritAndReturnExitCode(
+ "reindex",
+ "--index",
+ "groups",
+ "-d",
+ sitePaths.site_path.toString(),
+ "--show-stack-trace");
+
+ assertWithMessage("Slave hosts should allow to offline reindex groups")
+ .that(exitCode)
+ .isEqualTo(0);
+ }
+
+ @Test
+ public void offlineReindexForAllAvailableIndicesIsPossibleInSlaveMode() throws Exception {
+ enableSlaveMode();
+
+ int exitCode =
+ runGerritAndReturnExitCode(
+ "reindex", "-d", sitePaths.site_path.toString(), "--show-stack-trace");
+
+ assertWithMessage("Slave hosts should allow to perform a general offline reindex")
+ .that(exitCode)
+ .isEqualTo(0);
+ }
+
+ @Test
public void onlineUpgradeChanges() throws Exception {
int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion();
int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion();
@@ -143,12 +232,24 @@
}
private void setOnlineUpgradeConfig(boolean enable) throws Exception {
+ updateConfig(cfg -> cfg.setBoolean("index", null, "onlineUpgrade", enable));
+ }
+
+ private void enableSlaveMode() throws Exception {
+ updateConfig(config -> config.setBoolean("container", null, "slave", true));
+ }
+
+ private void updateConfig(Consumer<Config> configConsumer) throws Exception {
FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect());
cfg.load();
- cfg.setBoolean("index", null, "onlineUpgrade", enable);
+ configConsumer.accept(cfg);
cfg.save();
}
+ private static int runGerritAndReturnExitCode(String... args) throws Exception {
+ return GerritLauncher.mainImpl(args);
+ }
+
private void assertSearchVersion(ServerContext ctx, int expected) {
assertThat(
ctx.getInjector()
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index af609a6..dce65e1 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -107,8 +107,31 @@
}
@Test
+ public void createEmptyChange_InvalidSubject() throws Exception {
+ ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+ ci.subject = "Change-Id: I1234000000000000000000000000000000000000";
+ assertCreateFails(
+ ci,
+ ResourceConflictException.class,
+ "missing subject; Change-Id must be in commit message footer");
+ }
+
+ @Test
public void createNewChange() throws Exception {
- assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
+ ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
+ assertThat(info.revisions.get(info.currentRevision).commit.message)
+ .contains("Change-Id: " + info.changeId);
+ }
+
+ @Test
+ public void createNewChangeWithChangeId() throws Exception {
+ ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+ String changeId = "I1234000000000000000000000000000000000000";
+ String changeIdLine = "Change-Id: " + changeId;
+ ci.subject = "Subject\n\n" + changeIdLine;
+ ChangeInfo info = assertCreateSucceeds(ci);
+ assertThat(info.changeId).isEqualTo(changeId);
+ assertThat(info.revisions.get(info.currentRevision).commit.message).contains(changeIdLine);
}
@Test
@@ -136,14 +159,38 @@
@Test
public void createNewChangeSignedOffByFooter() throws Exception {
- setSignedOffByFooter();
+ setSignedOffByFooter(true);
+ try {
+ ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
+ String message = info.revisions.get(info.currentRevision).commit.message;
+ assertThat(message)
+ .contains(
+ String.format(
+ "%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.getIdent().getEmailAddress()));
+ } finally {
+ setSignedOffByFooter(false);
+ }
+ }
- ChangeInfo info = assertCreateSucceeds(newChangeInput(ChangeStatus.NEW));
- String message = info.revisions.get(info.currentRevision).commit.message;
- assertThat(message)
- .contains(
- String.format(
- "%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.getIdent().getEmailAddress()));
+ @Test
+ public void createNewChangeSignedOffByFooterWithChangeId() throws Exception {
+ setSignedOffByFooter(true);
+ try {
+ ChangeInput ci = newChangeInput(ChangeStatus.NEW);
+ String changeId = "I1234000000000000000000000000000000000000";
+ String changeIdLine = "Change-Id: " + changeId;
+ ci.subject = "Subject\n\n" + changeIdLine;
+ ChangeInfo info = assertCreateSucceeds(ci);
+ assertThat(info.changeId).isEqualTo(changeId);
+ String message = info.revisions.get(info.currentRevision).commit.message;
+ assertThat(message).contains(changeIdLine);
+ assertThat(message)
+ .contains(
+ String.format(
+ "%sAdministrator <%s>", SIGNED_OFF_BY_TAG, admin.getIdent().getEmailAddress()));
+ } finally {
+ setSignedOffByFooter(false);
+ }
}
@Test
@@ -378,7 +425,7 @@
ChangeInfo out = gApi.changes().create(in).get();
assertThat(out.project).isEqualTo(in.project);
assertThat(out.branch).isEqualTo(in.branch);
- assertThat(out.subject).isEqualTo(in.subject);
+ assertThat(out.subject).isEqualTo(in.subject.split("\n")[0]);
assertThat(out.topic).isEqualTo(in.topic);
assertThat(out.status).isEqualTo(in.status);
assertThat(out.isPrivate).isEqualTo(in.isPrivate);
@@ -398,17 +445,21 @@
}
// TODO(davido): Expose setting of account preferences in the API
- private void setSignedOffByFooter() throws Exception {
+ private void setSignedOffByFooter(boolean value) throws Exception {
RestResponse r = adminRestSession.get("/accounts/" + admin.email + "/preferences");
r.assertOK();
GeneralPreferencesInfo i = newGson().fromJson(r.getReader(), GeneralPreferencesInfo.class);
- i.signedOffBy = true;
+ i.signedOffBy = value;
r = adminRestSession.put("/accounts/" + admin.email + "/preferences", i);
r.assertOK();
GeneralPreferencesInfo o = newGson().fromJson(r.getReader(), GeneralPreferencesInfo.class);
- assertThat(o.signedOffBy).isTrue();
+ if (value) {
+ assertThat(o.signedOffBy).isTrue();
+ } else {
+ assertThat(o.signedOffBy).isNull();
+ }
resetCurrentApiUser();
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
index 822841c..2958888 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/IndexChangeIT.java
@@ -14,7 +14,21 @@
package com.google.gerrit.acceptance.rest.change;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.Permission;
+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.git.ProjectConfig;
+import com.google.gerrit.server.project.testing.Util;
+import java.util.List;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.junit.TestRepository;
import org.junit.Test;
public class IndexChangeIT extends AbstractDaemonTest {
@@ -30,4 +44,62 @@
blockRead("refs/heads/master");
userRestSession.post("/changes/" + changeId + "/index/").assertNotFound();
}
+
+ @Test
+ public void indexChangeAfterOwnerLosesVisibility() throws Exception {
+ // Create a test group with 2 users as members
+ TestAccount user2 = accountCreator.user2();
+ String group = createGroup("test");
+ gApi.groups().id(group).addMembers("admin", "user", user2.username);
+
+ // 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);
+
+ // Clone it and push a change as a regular user
+ TestRepository<InMemoryRepository> repo = cloneProject(p, user);
+ PushOneCommit push = pushFactory.create(db, user.getIdent(), repo);
+ PushOneCommit.Result result = push.to("refs/for/master");
+ result.assertOkStatus();
+ assertThat(result.getChange().change().getOwner()).isEqualTo(user.id);
+ String changeId = result.getChangeId();
+
+ // User can see the change and it is mergeable
+ setApiUser(user);
+ List<ChangeInfo> changes = gApi.changes().query(changeId).get();
+ assertThat(changes).hasSize(1);
+ assertThat(changes.get(0).mergeable).isNotNull();
+
+ // Other user can see the change and it is mergeable
+ setApiUser(user2);
+ changes = gApi.changes().query(changeId).get();
+ assertThat(changes).hasSize(1);
+ assertThat(changes.get(0).mergeable).isTrue();
+
+ // Remove the user from the group so they can no longer see the project
+ setApiUser(admin);
+ gApi.groups().id(group).removeMembers("user");
+
+ // User can no longer see the change
+ setApiUser(user);
+ changes = gApi.changes().query(changeId).get();
+ assertThat(changes).isEmpty();
+
+ // Reindex the change
+ setApiUser(admin);
+ gApi.changes().id(changeId).index();
+
+ // Other user can still see the change and it is still mergeable
+ setApiUser(user2);
+ changes = gApi.changes().query(changeId).get();
+ assertThat(changes).hasSize(1);
+ assertThat(changes.get(0).mergeable).isTrue();
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index c188d63..7657e2e 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -85,6 +85,17 @@
@Test
public void suggestReviewersChange() throws Exception {
String changeId = createChange().getChangeId();
+ testSuggestReviewersChange(changeId);
+ }
+
+ @Test
+ public void suggestReviewersPrivateChange() throws Exception {
+ String changeId = createChange().getChangeId();
+ gApi.changes().id(changeId).setPrivate(true, null);
+ testSuggestReviewersChange(changeId);
+ }
+
+ public void testSuggestReviewersChange(String changeId) throws Exception {
List<SuggestedReviewerInfo> reviewers = suggestReviewers(changeId, name("u"), 6);
assertReviewers(
reviewers, ImmutableList.of(user1, user2, user3), ImmutableList.of(group1, group2, group3));
@@ -133,7 +144,7 @@
}
@Test
- public void suggestReviewsPrivateProjectVisibility() throws Exception {
+ public void suggestReviewersPrivateProjectVisibility() throws Exception {
String changeId = createChange().getChangeId();
List<SuggestedReviewerInfo> reviewers;
diff --git a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
index 8fc5312..206b06c 100644
--- a/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -56,7 +56,6 @@
@GerritConfig(name = "auth.httpPasswordUrl", value = "https://example.com/password")
// change
- @GerritConfig(name = "change.allowDrafts", value = "false")
@GerritConfig(name = "change.largeChange", value = "300")
@GerritConfig(name = "change.replyTooltip", value = "Publish votes and draft comments")
@GerritConfig(name = "change.replyLabel", value = "Vote")
@@ -101,7 +100,6 @@
assertThat(i.auth.httpPasswordUrl).isNull();
// change
- assertThat(i.change.allowDrafts).isNull();
assertThat(i.change.largeChange).isEqualTo(300);
assertThat(i.change.replyTooltip).startsWith("Publish votes and draft comments");
assertThat(i.change.replyLabel).isEqualTo("Vote\u2026");
@@ -175,7 +173,6 @@
assertThat(i.auth.httpPasswordUrl).isNull();
// change
- assertThat(i.change.allowDrafts).isTrue();
assertThat(i.change.largeChange).isEqualTo(500);
assertThat(i.change.replyTooltip).startsWith("Reply and score");
assertThat(i.change.replyLabel).isEqualTo("Reply\u2026");
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 532ce45..c8ad4a4 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -16,6 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GitUtil;
@@ -33,6 +34,7 @@
import com.google.gerrit.extensions.api.projects.ProjectApi;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.WebLinkInfo;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.RegistrationHandle;
@@ -68,20 +70,18 @@
private static final String LABEL_CODE_REVIEW = "Code-Review";
- private String newProjectName;
- private ProjectApi pApi;
+ private Project.NameKey newProjectName;
@Inject private DynamicSet<FileHistoryWebLink> fileHistoryWebLinkDynamicSet;
@Before
public void setUp() throws Exception {
- newProjectName = createProject(PROJECT_NAME).get();
- pApi = gApi.projects().name(newProjectName);
+ newProjectName = createProject(PROJECT_NAME);
}
@Test
public void getDefaultInheritance() throws Exception {
- String inheritedName = pApi.access().inheritsFrom.name;
+ String inheritedName = pApi().access().inheritsFrom.name;
assertThat(inheritedName).isEqualTo(AllProjectsNameProvider.DEFAULT);
}
@@ -98,7 +98,7 @@
}
});
try {
- ProjectAccessInfo info = pApi.access();
+ ProjectAccessInfo info = pApi().access();
assertThat(info.configWebLinks).hasSize(1);
assertThat(info.configWebLinks.get(0).url)
.isEqualTo("http://view/" + newProjectName + "/project.config");
@@ -119,13 +119,13 @@
"name", "imageURL", "http://view/" + projectName + "/" + fileName);
}
});
- try (Repository repo = repoManager.openRepository(new Project.NameKey(newProjectName))) {
+ try (Repository repo = repoManager.openRepository(newProjectName)) {
RefUpdate u = repo.updateRef(RefNames.REFS_CONFIG);
u.setForceUpdate(true);
assertThat(u.delete()).isEqualTo(Result.FORCED);
// This should not crash.
- pApi.access();
+ pApi().access();
} finally {
handle.remove();
}
@@ -133,34 +133,34 @@
@Test
public void addAccessSection() throws Exception {
- Project.NameKey p = new Project.NameKey(newProjectName);
- RevCommit initialHead = getRemoteHead(p, RefNames.REFS_CONFIG);
+ RevCommit initialHead = getRemoteHead(newProjectName, RefNames.REFS_CONFIG);
ProjectAccessInput accessInput = newProjectAccessInput();
AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
accessInput.add.put(REFS_HEADS, accessSectionInfo);
- pApi.access(accessInput);
+ pApi().access(accessInput);
- assertThat(pApi.access().local).isEqualTo(accessInput.add);
+ assertThat(pApi().access().local).isEqualTo(accessInput.add);
- RevCommit updatedHead = getRemoteHead(p, RefNames.REFS_CONFIG);
+ RevCommit updatedHead = getRemoteHead(newProjectName, RefNames.REFS_CONFIG);
eventRecorder.assertRefUpdatedEvents(
- p.get(), RefNames.REFS_CONFIG, null, initialHead, initialHead, updatedHead);
+ newProjectName.get(), RefNames.REFS_CONFIG, null, initialHead, initialHead, updatedHead);
}
@Test
public void createAccessChangeNop() throws Exception {
ProjectAccessInput accessInput = newProjectAccessInput();
exception.expect(BadRequestException.class);
- pApi.accessChange(accessInput);
+ pApi().accessChange(accessInput);
}
@Test
public void createAccessChange() throws Exception {
+ allow(newProjectName, RefNames.REFS_CONFIG, Permission.READ, REGISTERED_USERS);
// User can see the branch
setApiUser(user);
- gApi.projects().name(newProjectName).branch("refs/heads/master").get();
+ pApi().branch("refs/heads/master").get();
ProjectAccessInput accessInput = newProjectAccessInput();
@@ -175,9 +175,9 @@
accessInput.add.put(REFS_HEADS, accessSection);
setApiUser(user);
- ChangeInfo out = pApi.accessChange(accessInput);
+ ChangeInfo out = pApi().accessChange(accessInput);
- assertThat(out.project).isEqualTo(newProjectName);
+ assertThat(out.project).isEqualTo(newProjectName.get());
assertThat(out.branch).isEqualTo(RefNames.REFS_CONFIG);
assertThat(out.status).isEqualTo(ChangeStatus.NEW);
assertThat(out.submitted).isNull();
@@ -195,7 +195,7 @@
// check that the change took effect.
setApiUser(user);
try {
- BranchInfo info = gApi.projects().name(newProjectName).branch("refs/heads/master").get();
+ BranchInfo info = pApi().branch("refs/heads/master").get();
fail("wanted failure, got " + newGson().toJson(info));
} catch (ResourceNotFoundException e) {
// OK.
@@ -206,17 +206,15 @@
accessInput.remove.put(REFS_HEADS, accessSection);
setApiUser(user);
- pApi.accessChange(accessInput);
-
setApiUser(admin);
- out = pApi.accessChange(accessInput);
+ out = pApi().accessChange(accessInput);
gApi.changes().id(out._number).current().review(reviewIn);
gApi.changes().id(out._number).current().submit();
// Now it works again.
setApiUser(user);
- gApi.projects().name(newProjectName).branch("refs/heads/master").get();
+ pApi().branch("refs/heads/master").get();
}
@Test
@@ -226,7 +224,7 @@
AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
accessInput.add.put(REFS_HEADS, accessSectionInfo);
- pApi.access(accessInput);
+ pApi().access(accessInput);
// Remove specific permission
AccessSectionInfo accessSectionToRemove = newAccessSectionInfo();
@@ -234,13 +232,13 @@
Permission.LABEL + LABEL_CODE_REVIEW, newPermissionInfo());
ProjectAccessInput removal = newProjectAccessInput();
removal.remove.put(REFS_HEADS, accessSectionToRemove);
- pApi.access(removal);
+ pApi().access(removal);
// Remove locally
accessInput.add.get(REFS_HEADS).permissions.remove(Permission.LABEL + LABEL_CODE_REVIEW);
// Check
- assertThat(pApi.access().local).isEqualTo(accessInput.add);
+ assertThat(pApi().access().local).isEqualTo(accessInput.add);
}
@Test
@@ -250,7 +248,7 @@
AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
accessInput.add.put(REFS_HEADS, accessSectionInfo);
- pApi.access(accessInput);
+ pApi().access(accessInput);
// Remove specific permission rule
AccessSectionInfo accessSectionToRemove = newAccessSectionInfo();
@@ -261,7 +259,7 @@
accessSectionToRemove.permissions.put(Permission.LABEL + LABEL_CODE_REVIEW, codeReview);
ProjectAccessInput removal = newProjectAccessInput();
removal.remove.put(REFS_HEADS, accessSectionToRemove);
- pApi.access(removal);
+ pApi().access(removal);
// Remove locally
accessInput
@@ -273,7 +271,7 @@
.remove(SystemGroupBackend.REGISTERED_USERS.get());
// Check
- assertThat(pApi.access().local).isEqualTo(accessInput.add);
+ assertThat(pApi().access().local).isEqualTo(accessInput.add);
}
@Test
@@ -283,7 +281,7 @@
AccessSectionInfo accessSectionInfo = createDefaultAccessSectionInfo();
accessInput.add.put(REFS_HEADS, accessSectionInfo);
- pApi.access(accessInput);
+ pApi().access(accessInput);
// Remove specific permission rules
AccessSectionInfo accessSectionToRemove = newAccessSectionInfo();
@@ -296,13 +294,13 @@
accessSectionToRemove.permissions.put(Permission.LABEL + LABEL_CODE_REVIEW, codeReview);
ProjectAccessInput removal = newProjectAccessInput();
removal.remove.put(REFS_HEADS, accessSectionToRemove);
- pApi.access(removal);
+ pApi().access(removal);
// Remove locally
accessInput.add.get(REFS_HEADS).permissions.remove(Permission.LABEL + LABEL_CODE_REVIEW);
// Check
- assertThat(pApi.access().local).isEqualTo(accessInput.add);
+ assertThat(pApi().access().local).isEqualTo(accessInput.add);
}
@Test
@@ -313,11 +311,11 @@
// Disallow READ
accessInput.add.put(REFS_ALL, accessSectionInfo);
- pApi.access(accessInput);
+ pApi().access(accessInput);
setApiUser(user);
exception.expect(ResourceNotFoundException.class);
- gApi.projects().name(newProjectName).access();
+ pApi().access();
}
@Test
@@ -328,7 +326,7 @@
// Disallow READ
accessInput.add.put(REFS_ALL, accessSectionInfo);
- pApi.access(accessInput);
+ pApi().access(accessInput);
// Create a change to apply
ProjectAccessInput accessInfoToApply = newProjectAccessInput();
@@ -337,7 +335,7 @@
setApiUser(user);
exception.expect(ResourceNotFoundException.class);
- gApi.projects().name(newProjectName).access();
+ pApi().access();
}
@Test
@@ -357,7 +355,7 @@
accessSection.permissions.put(Permission.READ, read);
accessInput.add.put(REFS_ALL, accessSection);
- ProjectAccessInfo result = pApi.access(accessInput);
+ ProjectAccessInfo result = pApi().access(accessInput);
assertThat(result.groups.keySet())
.containsExactly(
SystemGroupBackend.PROJECT_OWNERS.get(), SystemGroupBackend.ANONYMOUS_USERS.get());
@@ -370,19 +368,23 @@
assertThat(result.groups.get(SystemGroupBackend.PROJECT_OWNERS.get()).id).isNull();
// Get call returns groups too.
- ProjectAccessInfo loggedInResult = pApi.access();
+ ProjectAccessInfo loggedInResult = pApi().access();
assertThat(loggedInResult.groups.keySet())
.containsExactly(
SystemGroupBackend.PROJECT_OWNERS.get(), SystemGroupBackend.ANONYMOUS_USERS.get());
- assertThat(loggedInResult.groups.get(SystemGroupBackend.PROJECT_OWNERS.get()).name)
- .isEqualTo("Project Owners");
- assertThat(loggedInResult.groups.get(SystemGroupBackend.PROJECT_OWNERS.get()).id).isNull();
- // PROJECT_OWNERS is invisible to anonymous user, so we strip it.
+ GroupInfo owners = loggedInResult.groups.get(SystemGroupBackend.PROJECT_OWNERS.get());
+ assertThat(owners.name).isEqualTo("Project Owners");
+ assertThat(owners.id).isNull();
+ assertThat(owners.members).isNull();
+ assertThat(owners.includes).isNull();
+
+ // PROJECT_OWNERS is invisible to anonymous user, but GetAccess disregards visibility.
setApiUserAnonymous();
- ProjectAccessInfo anonResult = pApi.access();
+ ProjectAccessInfo anonResult = pApi().access();
assertThat(anonResult.groups.keySet())
- .containsExactly(SystemGroupBackend.ANONYMOUS_USERS.get());
+ .containsExactly(
+ SystemGroupBackend.PROJECT_OWNERS.get(), SystemGroupBackend.ANONYMOUS_USERS.get());
}
@Test
@@ -397,7 +399,7 @@
setApiUser(user);
exception.expect(AuthException.class);
exception.expectMessage("administrate server not permitted");
- gApi.projects().name(newProjectName).access(accessInput);
+ pApi().access(accessInput);
}
@Test
@@ -409,9 +411,9 @@
ProjectAccessInput accessInput = newProjectAccessInput();
accessInput.parent = newParentProjectName;
- gApi.projects().name(newProjectName).access(accessInput);
+ pApi().access(accessInput);
- assertThat(pApi.access().inheritsFrom.name).isEqualTo(newParentProjectName);
+ assertThat(pApi().access().inheritsFrom.name).isEqualTo(newParentProjectName);
}
@Test
@@ -452,7 +454,7 @@
accessInput.add.put(AccessSection.GLOBAL_CAPABILITIES, accessSectionInfo);
exception.expect(BadRequestException.class);
- pApi.access(accessInput);
+ pApi().access(accessInput);
}
@Test
@@ -627,6 +629,10 @@
assertThat(permissions2.keySet()).containsExactly(Permission.READ);
}
+ private ProjectApi pApi() throws Exception {
+ return gApi.projects().name(newProjectName.get());
+ }
+
private ProjectAccessInput newProjectAccessInput() {
ProjectAccessInput p = new ProjectAccessInput();
p.add = new HashMap<>();
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
index a96c6ec..df4076d 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ListMailFilterIT.java
@@ -68,6 +68,9 @@
// Check that the comments from the email have NOT been persisted
Collection<ChangeMessageInfo> messages = gApi.changes().id(changeInfo.id).get().messages;
assertThat(messages).hasSize(2);
+
+ // Check that no emails were sent because of this error
+ assertThat(sender.getMessages()).isEmpty();
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
index 9de4797..f34fe33 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailProcessorIT.java
@@ -23,6 +23,7 @@
import com.google.gerrit.server.mail.MailUtil;
import com.google.gerrit.server.mail.receive.MailMessage;
import com.google.gerrit.server.mail.receive.MailProcessor;
+import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.inject.Inject;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -226,4 +227,34 @@
assertNotifyTo(admin);
}
+
+ @Test
+ public void sendNotificationOnMissingMetadatas() throws Exception {
+ String changeId = createChangeWithReview();
+ ChangeInfo changeInfo = gApi.changes().id(changeId).get();
+ List<CommentInfo> comments = gApi.changes().id(changeId).current().commentsAsList();
+ assertThat(comments).hasSize(2);
+ String ts = "null"; // Erroneous timestamp to be used in erroneous metadatas
+
+ // Build Message
+ String txt =
+ newPlaintextBody(
+ canonicalWebUrl.get() + "#/c/" + changeInfo._number + "/1",
+ "Test Message",
+ null,
+ null,
+ null);
+ MailMessage.Builder b =
+ messageBuilderWithDefaultFields()
+ .from(user.emailAddress)
+ .textContent(txt + textFooterForChange(changeInfo._number, ts));
+
+ sender.clear();
+ mailProcessor.process(b.build());
+
+ assertNotifyTo(user);
+ Message message = sender.nextMessage();
+ assertThat(message.body()).contains("was unable to parse your email");
+ assertThat(message.headers()).containsKey("Subject");
+ }
}
diff --git a/javatests/com/google/gerrit/server/config/ConfigUtilTest.java b/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
index fc0b1dcd..231b584 100644
--- a/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/javatests/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -20,7 +20,6 @@
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.junit.Assert.assertEquals;
import com.google.gerrit.extensions.client.Theme;
import java.util.List;
@@ -142,49 +141,49 @@
@Test
public void timeUnit() {
- assertEquals(ms(0, MILLISECONDS), parse("0"));
- assertEquals(ms(2, MILLISECONDS), parse("2ms"));
- assertEquals(ms(200, MILLISECONDS), parse("200 milliseconds"));
+ assertThat(parse("0")).isEqualTo(ms(0, MILLISECONDS));
+ assertThat(parse("2ms")).isEqualTo(ms(2, MILLISECONDS));
+ assertThat(parse("200 milliseconds")).isEqualTo(ms(200, MILLISECONDS));
- assertEquals(ms(0, SECONDS), parse("0s"));
- assertEquals(ms(2, SECONDS), parse("2s"));
- assertEquals(ms(231, SECONDS), parse("231sec"));
- assertEquals(ms(1, SECONDS), parse("1second"));
- assertEquals(ms(300, SECONDS), parse("300 seconds"));
+ assertThat(parse("0s")).isEqualTo(ms(0, SECONDS));
+ assertThat(parse("2s")).isEqualTo(ms(2, SECONDS));
+ assertThat(parse("231sec")).isEqualTo(ms(231, SECONDS));
+ assertThat(parse("1second")).isEqualTo(ms(1, SECONDS));
+ assertThat(parse("300 seconds")).isEqualTo(ms(300, SECONDS));
- assertEquals(ms(2, MINUTES), parse("2m"));
- assertEquals(ms(2, MINUTES), parse("2min"));
- assertEquals(ms(1, MINUTES), parse("1 minute"));
- assertEquals(ms(10, MINUTES), parse("10 minutes"));
+ assertThat(parse("2m")).isEqualTo(ms(2, MINUTES));
+ assertThat(parse("2min")).isEqualTo(ms(2, MINUTES));
+ assertThat(parse("1 minute")).isEqualTo(ms(1, MINUTES));
+ assertThat(parse("10 minutes")).isEqualTo(ms(10, MINUTES));
- assertEquals(ms(5, HOURS), parse("5h"));
- assertEquals(ms(5, HOURS), parse("5hr"));
- assertEquals(ms(1, HOURS), parse("1hour"));
- assertEquals(ms(48, HOURS), parse("48hours"));
+ assertThat(parse("5h")).isEqualTo(ms(5, HOURS));
+ assertThat(parse("5hr")).isEqualTo(ms(5, HOURS));
+ assertThat(parse("1hour")).isEqualTo(ms(1, HOURS));
+ assertThat(parse("48hours")).isEqualTo(ms(48, HOURS));
- assertEquals(ms(5, HOURS), parse("5 h"));
- assertEquals(ms(5, HOURS), parse("5 hr"));
- assertEquals(ms(1, HOURS), parse("1 hour"));
- assertEquals(ms(48, HOURS), parse("48 hours"));
- assertEquals(ms(48, HOURS), parse("48 \t \r hours"));
+ assertThat(parse("5 h")).isEqualTo(ms(5, HOURS));
+ assertThat(parse("5 hr")).isEqualTo(ms(5, HOURS));
+ assertThat(parse("1 hour")).isEqualTo(ms(1, HOURS));
+ assertThat(parse("48 hours")).isEqualTo(ms(48, HOURS));
+ assertThat(parse("48 \t \r hours")).isEqualTo(ms(48, HOURS));
- assertEquals(ms(4, DAYS), parse("4d"));
- assertEquals(ms(1, DAYS), parse("1day"));
- assertEquals(ms(14, DAYS), parse("14days"));
+ assertThat(parse("4d")).isEqualTo(ms(4, DAYS));
+ assertThat(parse("1day")).isEqualTo(ms(1, DAYS));
+ assertThat(parse("14days")).isEqualTo(ms(14, DAYS));
- assertEquals(ms(7, DAYS), parse("1w"));
- assertEquals(ms(7, DAYS), parse("1week"));
- assertEquals(ms(14, DAYS), parse("2w"));
- assertEquals(ms(14, DAYS), parse("2weeks"));
+ assertThat(parse("1w")).isEqualTo(ms(7, DAYS));
+ assertThat(parse("1week")).isEqualTo(ms(7, DAYS));
+ assertThat(parse("2w")).isEqualTo(ms(14, DAYS));
+ assertThat(parse("2weeks")).isEqualTo(ms(14, DAYS));
- assertEquals(ms(30, DAYS), parse("1mon"));
- assertEquals(ms(30, DAYS), parse("1month"));
- assertEquals(ms(60, DAYS), parse("2mon"));
- assertEquals(ms(60, DAYS), parse("2months"));
+ assertThat(parse("1mon")).isEqualTo(ms(30, DAYS));
+ assertThat(parse("1month")).isEqualTo(ms(30, DAYS));
+ assertThat(parse("2mon")).isEqualTo(ms(60, DAYS));
+ assertThat(parse("2months")).isEqualTo(ms(60, DAYS));
- assertEquals(ms(365, DAYS), parse("1y"));
- assertEquals(ms(365, DAYS), parse("1year"));
- assertEquals(ms(365 * 2, DAYS), parse("2years"));
+ assertThat(parse("1y")).isEqualTo(ms(365, DAYS));
+ assertThat(parse("1year")).isEqualTo(ms(365, DAYS));
+ assertThat(parse("2years")).isEqualTo(ms(365 * 2, DAYS));
}
private static long ms(int cnt, TimeUnit unit) {
diff --git a/javatests/com/google/gerrit/server/config/GitwebConfigTest.java b/javatests/com/google/gerrit/server/config/GitwebConfigTest.java
index ab7da99..cb6de34 100644
--- a/javatests/com/google/gerrit/server/config/GitwebConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/GitwebConfigTest.java
@@ -14,27 +14,29 @@
package com.google.gerrit.server.config;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertWithMessage;
import org.junit.Test;
public class GitwebConfigTest {
-
private static final String VALID_CHARACTERS = "*()";
private static final String SOME_INVALID_CHARACTERS = "09AZaz$-_.+!',";
@Test
public void validPathSeparator() {
for (char c : VALID_CHARACTERS.toCharArray()) {
- assertTrue("valid character rejected: " + c, GitwebConfig.isValidPathSeparator(c));
+ assertWithMessage("valid character rejected: " + c)
+ .that(GitwebConfig.isValidPathSeparator(c))
+ .isTrue();
}
}
@Test
public void inalidPathSeparator() {
for (char c : SOME_INVALID_CHARACTERS.toCharArray()) {
- assertFalse("invalid character accepted: " + c, GitwebConfig.isValidPathSeparator(c));
+ assertWithMessage("invalid character accepted: " + c)
+ .that(GitwebConfig.isValidPathSeparator(c))
+ .isFalse();
}
}
}
diff --git a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
index bcba665..577d931 100644
--- a/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
+++ b/javatests/com/google/gerrit/server/config/ListCapabilitiesTest.java
@@ -14,9 +14,7 @@
package com.google.gerrit.server.config;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.Exports;
@@ -60,14 +58,14 @@
Map<String, CapabilityInfo> m =
injector.getInstance(ListCapabilities.class).apply(new ConfigResource());
for (String id : GlobalCapability.getAllNames()) {
- assertTrue("contains " + id, m.containsKey(id));
- assertEquals(id, m.get(id).id);
- assertNotNull(id + " has name", m.get(id).name);
+ assertThat(m).containsKey(id);
+ assertThat(m.get(id).id).isEqualTo(id);
+ assertThat(m.get(id).name).isNotNull();
}
String pluginCapability = "gerrit-printHello";
- assertTrue("contains " + pluginCapability, m.containsKey(pluginCapability));
- assertEquals(pluginCapability, m.get(pluginCapability).id);
- assertEquals("Print Hello", m.get(pluginCapability).name);
+ assertThat(m).containsKey(pluginCapability);
+ assertThat(m.get(pluginCapability).id).isEqualTo(pluginCapability);
+ assertThat(m.get(pluginCapability).name).isEqualTo("Print Hello");
}
}
diff --git a/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java b/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
index 0423a53..70893a9 100644
--- a/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
+++ b/javatests/com/google/gerrit/server/config/ScheduleConfigTest.java
@@ -14,16 +14,19 @@
package com.google.gerrit.server.config;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth8.assertThat;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
-import static org.junit.Assert.assertEquals;
+import com.google.gerrit.server.config.ScheduleConfig.Schedule;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
+import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.Config;
import org.junit.Test;
@@ -36,45 +39,186 @@
@Test
public void initialDelay() throws Exception {
- assertEquals(ms(1, HOURS), initialDelay("11:00", "1h"));
- assertEquals(ms(30, MINUTES), initialDelay("05:30", "1h"));
- assertEquals(ms(30, MINUTES), initialDelay("09:30", "1h"));
- assertEquals(ms(30, MINUTES), initialDelay("13:30", "1h"));
- assertEquals(ms(59, MINUTES), initialDelay("13:59", "1h"));
+ assertThat(initialDelay("11:00", "1h")).isEqualTo(ms(1, HOURS));
+ assertThat(initialDelay("05:30", "1h")).isEqualTo(ms(30, MINUTES));
+ assertThat(initialDelay("09:30", "1h")).isEqualTo(ms(30, MINUTES));
+ assertThat(initialDelay("13:30", "1h")).isEqualTo(ms(30, MINUTES));
+ assertThat(initialDelay("13:59", "1h")).isEqualTo(ms(59, MINUTES));
- assertEquals(ms(1, HOURS), initialDelay("11:00", "1d"));
- assertEquals(ms(19, HOURS) + ms(30, MINUTES), initialDelay("05:30", "1d"));
+ assertThat(initialDelay("11:00", "1d")).isEqualTo(ms(1, HOURS));
+ assertThat(initialDelay("05:30", "1d")).isEqualTo(ms(19, HOURS) + ms(30, MINUTES));
- assertEquals(ms(1, HOURS), initialDelay("11:00", "1w"));
- assertEquals(ms(7, DAYS) - ms(4, HOURS) - ms(30, MINUTES), initialDelay("05:30", "1w"));
+ assertThat(initialDelay("11:00", "1w")).isEqualTo(ms(1, HOURS));
+ assertThat(initialDelay("05:30", "1w")).isEqualTo(ms(7, DAYS) - ms(4, HOURS) - ms(30, MINUTES));
- assertEquals(ms(3, DAYS) + ms(1, HOURS), initialDelay("Mon 11:00", "1w"));
- assertEquals(ms(1, HOURS), initialDelay("Fri 11:00", "1w"));
+ assertThat(initialDelay("Mon 11:00", "1w")).isEqualTo(ms(3, DAYS) + ms(1, HOURS));
+ assertThat(initialDelay("Fri 11:00", "1w")).isEqualTo(ms(1, HOURS));
- assertEquals(ms(1, HOURS), initialDelay("Mon 11:00", "1d"));
- assertEquals(ms(23, HOURS), initialDelay("Mon 09:00", "1d"));
- assertEquals(ms(1, DAYS), initialDelay("Mon 10:00", "1d"));
- assertEquals(ms(1, DAYS), initialDelay("Mon 10:00", "1d"));
+ assertThat(initialDelay("Mon 11:00", "1d")).isEqualTo(ms(1, HOURS));
+ assertThat(initialDelay("Mon 09:00", "1d")).isEqualTo(ms(23, HOURS));
+ assertThat(initialDelay("Mon 10:00", "1d")).isEqualTo(ms(1, DAYS));
+ assertThat(initialDelay("Mon 10:00", "1d")).isEqualTo(ms(1, DAYS));
}
@Test
- public void customKeys() {
+ public void defaultKeysWithoutSubsection() {
+ Config rc = new Config();
+ rc.setString("a", null, ScheduleConfig.KEY_INTERVAL, "1h");
+ rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "01:00");
+
+ assertThat(ScheduleConfig.builder(rc, "a").setNow(NOW).buildSchedule())
+ .hasValue(Schedule.create(ms(1, HOURS), ms(1, HOURS)));
+ }
+
+ @Test
+ public void defaultKeysWithSubsection() {
+ Config rc = new Config();
+ rc.setString("a", "b", ScheduleConfig.KEY_INTERVAL, "1h");
+ rc.setString("a", "b", ScheduleConfig.KEY_STARTTIME, "01:00");
+
+ assertThat(ScheduleConfig.builder(rc, "a").setSubsection("b").setNow(NOW).buildSchedule())
+ .hasValue(Schedule.create(ms(1, HOURS), ms(1, HOURS)));
+ }
+
+ @Test
+ public void customKeysWithoutSubsection() {
+ Config rc = new Config();
+ rc.setString("a", null, "i", "1h");
+ rc.setString("a", null, "s", "01:00");
+
+ assertThat(
+ ScheduleConfig.builder(rc, "a")
+ .setKeyInterval("i")
+ .setKeyStartTime("s")
+ .setNow(NOW)
+ .buildSchedule())
+ .hasValue(Schedule.create(ms(1, HOURS), ms(1, HOURS)));
+ }
+
+ @Test
+ public void customKeysWithSubsection() {
Config rc = new Config();
rc.setString("a", "b", "i", "1h");
rc.setString("a", "b", "s", "01:00");
- ScheduleConfig s = new ScheduleConfig(rc, "a", "b", "i", "s", NOW);
- assertEquals(ms(1, HOURS), s.getInterval());
- assertEquals(ms(1, HOURS), s.getInitialDelay());
+ assertThat(
+ ScheduleConfig.builder(rc, "a")
+ .setSubsection("b")
+ .setKeyInterval("i")
+ .setKeyStartTime("s")
+ .setNow(NOW)
+ .buildSchedule())
+ .hasValue(Schedule.create(ms(1, HOURS), ms(1, HOURS)));
+ }
- s = new ScheduleConfig(rc, "a", "b", "myInterval", "myStart", NOW);
- assertEquals(s.getInterval(), ScheduleConfig.MISSING_CONFIG);
- assertEquals(s.getInitialDelay(), ScheduleConfig.MISSING_CONFIG);
+ @Test
+ public void missingConfigWithoutSubsection() {
+ Config rc = new Config();
+ rc.setString("a", null, ScheduleConfig.KEY_INTERVAL, "1h");
+ rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "01:00");
+
+ assertThat(
+ ScheduleConfig.builder(rc, "a")
+ .setKeyInterval("myInterval")
+ .setKeyStartTime("myStart")
+ .buildSchedule())
+ .isEmpty();
+
+ assertThat(ScheduleConfig.builder(rc, "x").buildSchedule()).isEmpty();
+ }
+
+ @Test
+ public void missingConfigWithSubsection() {
+ Config rc = new Config();
+ rc.setString("a", "b", ScheduleConfig.KEY_INTERVAL, "1h");
+ rc.setString("a", "b", ScheduleConfig.KEY_STARTTIME, "01:00");
+
+ assertThat(
+ ScheduleConfig.builder(rc, "a")
+ .setSubsection("b")
+ .setKeyInterval("myInterval")
+ .setKeyStartTime("myStart")
+ .buildSchedule())
+ .isEmpty();
+
+ assertThat(ScheduleConfig.builder(rc, "a").setSubsection("x").buildSchedule()).isEmpty();
+
+ assertThat(ScheduleConfig.builder(rc, "x").setSubsection("b").buildSchedule()).isEmpty();
+ }
+
+ @Test
+ public void incompleteConfigMissingInterval() {
+ Config rc = new Config();
+ rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "01:00");
+
+ assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
+ }
+
+ @Test
+ public void incompleteConfigMissingStartTime() {
+ Config rc = new Config();
+ rc.setString("a", null, ScheduleConfig.KEY_INTERVAL, "1h");
+
+ assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
+ }
+
+ @Test
+ public void invalidConfigBadInterval() {
+ Config rc = new Config();
+ rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "01:00");
+
+ rc.setString("a", null, ScheduleConfig.KEY_INTERVAL, "x");
+ assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
+
+ rc.setString("a", null, ScheduleConfig.KEY_INTERVAL, "1x");
+ assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
+
+ rc.setString("a", null, ScheduleConfig.KEY_INTERVAL, "0");
+ assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
+
+ rc.setString("a", null, ScheduleConfig.KEY_INTERVAL, "-1");
+ assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
+ }
+
+ @Test
+ public void invalidConfigBadStartTime() {
+ Config rc = new Config();
+ rc.setString("a", null, ScheduleConfig.KEY_INTERVAL, "1h");
+
+ rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "x");
+ assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
+
+ rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "Foo 01:00");
+ assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
+
+ rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "Mon 01:000");
+ assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
+
+ rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "001:00");
+ assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
+
+ rc.setString("a", null, ScheduleConfig.KEY_STARTTIME, "0100");
+ assertThat(ScheduleConfig.builder(rc, "a").buildSchedule()).isEmpty();
+ }
+
+ @Test
+ public void createInvalidSchedule() {
+ assertThat(Schedule.create(-1, "00:00")).isEmpty();
+ assertThat(Schedule.create(1, "x")).isEmpty();
+ assertThat(Schedule.create(1, "Foo 00:00")).isEmpty();
+ assertThat(Schedule.create(0, "Mon 00:000")).isEmpty();
+ assertThat(Schedule.create(1, "000:00")).isEmpty();
+ assertThat(Schedule.create(1, "0000")).isEmpty();
}
private static long initialDelay(String startTime, String interval) {
- return new ScheduleConfig(config(startTime, interval), "section", "subsection", NOW)
- .getInitialDelay();
+ Optional<Schedule> schedule =
+ ScheduleConfig.builder(config(startTime, interval), "section")
+ .setSubsection("subsection")
+ .setNow(NOW)
+ .buildSchedule();
+ assertThat(schedule).isPresent();
+ return schedule.get().initialDelay();
}
private static Config config(String startTime, String interval) {
diff --git a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
index c3d9ba5..5772a80 100644
--- a/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
+++ b/javatests/com/google/gerrit/server/extensions/webui/UiActionsTest.java
@@ -25,12 +25,16 @@
import com.google.gerrit.server.permissions.PermissionBackend;
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.PermissionBackendCondition;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import java.util.Collection;
+import java.util.Map;
import java.util.Set;
import org.easymock.EasyMock;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
import org.junit.Test;
public class UiActionsTest {
@@ -85,6 +89,12 @@
return ImmutableSet.of(ProjectPermission.READ);
}
+ @Override
+ public Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
+ throws PermissionBackendException {
+ throw new UnsupportedOperationException("not implemented");
+ }
+
private void disallowValueQueries() {
allowValueQueries = false;
}
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
index 5ecafd0..031284d 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -24,6 +24,7 @@
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.testing.GerritBaseTests;
@@ -60,7 +61,7 @@
.containsExactly(
"REVIEWER,1", "REVIEWER,1," + t1.getTime(), "CC,2", "CC,2," + t2.getTime());
- assertThat(ChangeField.parseReviewerFieldValues(values)).isEqualTo(reviewers);
+ assertThat(ChangeField.parseReviewerFieldValues(new Change.Id(1), values)).isEqualTo(reviewers);
}
@Test
diff --git a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index b525504..edd4abf 100644
--- a/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/javatests/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -27,7 +27,7 @@
new FakeQueryBuilder.Definition<>(FakeQueryBuilder.class),
new ChangeQueryBuilder.Arguments(
null, null, null, null, null, null, null, null, null, null, null, null, null, null,
- null, null, null, null, null, indexes, null, null, null, null, null, null, null, null));
+ null, null, null, null, null, indexes, null, null, null, null, null, null, null));
}
@Operator
diff --git a/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java b/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
new file mode 100644
index 0000000..a7234f4
--- /dev/null
+++ b/javatests/com/google/gerrit/server/mail/AutoReplyMailFilterTest.java
@@ -0,0 +1,71 @@
+// 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.mail;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.server.mail.receive.MailMessage;
+import com.google.gerrit.testing.GerritBaseTests;
+import java.time.Instant;
+import org.junit.Test;
+
+public class AutoReplyMailFilterTest extends GerritBaseTests {
+
+ private AutoReplyMailFilter autoReplyMailFilter = new AutoReplyMailFilter();
+
+ @Test
+ public void acceptsHumanReply() {
+ MailMessage.Builder b = createChangeAndReplyByEmail();
+ assertThat(autoReplyMailFilter.shouldProcessMessage(b.build())).isTrue();
+ }
+
+ @Test
+ public void discardsBulk() {
+ MailMessage.Builder b = createChangeAndReplyByEmail();
+ b.addAdditionalHeader("Precedence: bulk");
+ assertThat(autoReplyMailFilter.shouldProcessMessage(b.build())).isFalse();
+
+ b = createChangeAndReplyByEmail();
+ b.addAdditionalHeader("Precedence: list");
+ assertThat(autoReplyMailFilter.shouldProcessMessage(b.build())).isFalse();
+
+ b = createChangeAndReplyByEmail();
+ b.addAdditionalHeader("Precedence: junk");
+ assertThat(autoReplyMailFilter.shouldProcessMessage(b.build())).isFalse();
+ }
+
+ @Test
+ public void discardsAutoSubmitted() {
+ MailMessage.Builder b = createChangeAndReplyByEmail();
+ b.addAdditionalHeader("Auto-Submitted: yes");
+ assertThat(autoReplyMailFilter.shouldProcessMessage(b.build())).isFalse();
+
+ b = createChangeAndReplyByEmail();
+ b.addAdditionalHeader("Auto-Submitted: no");
+ assertThat(autoReplyMailFilter.shouldProcessMessage(b.build())).isTrue();
+ }
+
+ private MailMessage.Builder createChangeAndReplyByEmail() {
+ // Build Message
+ MailMessage.Builder b = MailMessage.builder();
+ b.id("some id");
+ b.from(new Address("admim@example.com"));
+ b.addTo(new Address("gerrit@my-company.com")); // Not evaluated
+ b.subject("");
+ b.dateReceived(Instant.now());
+ b.textContent("I am currently out of office, please leave a code review after the beep.");
+ return b;
+ }
+}
diff --git a/javatests/com/google/gerrit/server/mail/receive/HtmlParserTest.java b/javatests/com/google/gerrit/server/mail/receive/HtmlParserTest.java
index b9548bd..d88e09f 100644
--- a/javatests/com/google/gerrit/server/mail/receive/HtmlParserTest.java
+++ b/javatests/com/google/gerrit/server/mail/receive/HtmlParserTest.java
@@ -83,6 +83,32 @@
}
@Test
+ public void simpleInlineCommentsWithLink() {
+ MailMessage.Builder b = newMailMessageBuilder();
+ b.htmlContent(
+ newHtmlBody(
+ "Looks good to me",
+ "How about [1]? This would help IMHO.</div><div>[1] "
+ + "<a href=\"http://gerritcodereview.com\">http://gerritcodereview.com</a>",
+ null,
+ "Also have a comment here.",
+ null,
+ null,
+ null));
+
+ List<Comment> comments = defaultComments();
+ List<MailComment> parsedComments = HtmlParser.parse(b.build(), comments, CHANGE_URL);
+
+ assertThat(parsedComments).hasSize(3);
+ assertChangeMessage("Looks good to me", parsedComments.get(0));
+ assertInlineComment(
+ "How about [1]? This would help IMHO.\n\n[1] http://gerritcodereview.com",
+ parsedComments.get(1),
+ comments.get(1));
+ assertInlineComment("Also have a comment here.", parsedComments.get(2), comments.get(4));
+ }
+
+ @Test
public void simpleFileComment() {
MailMessage.Builder b = newMailMessageBuilder();
b.htmlContent(
diff --git a/javatests/com/google/gerrit/server/mail/receive/MetadataParserTest.java b/javatests/com/google/gerrit/server/mail/receive/MailHeaderParserTest.java
similarity index 71%
rename from javatests/com/google/gerrit/server/mail/receive/MetadataParserTest.java
rename to javatests/com/google/gerrit/server/mail/receive/MailHeaderParserTest.java
index dc25939..b7277f3 100644
--- a/javatests/com/google/gerrit/server/mail/receive/MetadataParserTest.java
+++ b/javatests/com/google/gerrit/server/mail/receive/MailHeaderParserTest.java
@@ -15,18 +15,16 @@
package com.google.gerrit.server.mail.receive;
import static com.google.common.truth.Truth.assertThat;
-import static com.google.gerrit.server.mail.MetadataName.toFooterWithDelimiter;
-import static com.google.gerrit.server.mail.MetadataName.toHeaderWithDelimiter;
import com.google.gerrit.server.mail.Address;
-import com.google.gerrit.server.mail.MetadataName;
+import com.google.gerrit.server.mail.MailHeader;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneOffset;
import org.junit.Test;
-public class MetadataParserTest {
+public class MailHeaderParserTest {
@Test
public void parseMetadataFromHeader() {
// This tests if the metadata parser is able to parse metadata from the
@@ -36,16 +34,16 @@
b.dateReceived(Instant.now());
b.subject("");
- b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.CHANGE_NUMBER) + "123");
- b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.PATCH_SET) + "1");
- b.addAdditionalHeader(toHeaderWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment");
+ b.addAdditionalHeader(MailHeader.CHANGE_NUMBER.fieldWithDelimiter() + "123");
+ b.addAdditionalHeader(MailHeader.PATCH_SET.fieldWithDelimiter() + "1");
+ b.addAdditionalHeader(MailHeader.MESSAGE_TYPE.fieldWithDelimiter() + "comment");
b.addAdditionalHeader(
- toHeaderWithDelimiter(MetadataName.TIMESTAMP) + "Tue, 25 Oct 2016 02:11:35 -0700");
+ MailHeader.COMMENT_DATE.fieldWithDelimiter() + "Tue, 25 Oct 2016 02:11:35 -0700");
Address author = new Address("Diffy", "test@gerritcodereview.com");
b.from(author);
- MailMetadata meta = MetadataParser.parse(b.build());
+ MailMetadata meta = MailHeaderParser.parse(b.build());
assertThat(meta.author).isEqualTo(author.getEmail());
assertThat(meta.changeNumber).isEqualTo(123);
assertThat(meta.patchSet).isEqualTo(1);
@@ -67,17 +65,17 @@
b.subject("");
StringBuilder stringBuilder = new StringBuilder();
- stringBuilder.append(toFooterWithDelimiter(MetadataName.CHANGE_NUMBER) + "123\r\n");
- stringBuilder.append("> " + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1\n");
- stringBuilder.append(toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment\n");
+ stringBuilder.append(MailHeader.CHANGE_NUMBER.withDelimiter() + "123\r\n");
+ stringBuilder.append("> " + MailHeader.PATCH_SET.withDelimiter() + "1\n");
+ stringBuilder.append(MailHeader.MESSAGE_TYPE.withDelimiter() + "comment\n");
stringBuilder.append(
- toFooterWithDelimiter(MetadataName.TIMESTAMP) + "Tue, 25 Oct 2016 02:11:35 -0700\r\n");
+ MailHeader.COMMENT_DATE.withDelimiter() + "Tue, 25 Oct 2016 02:11:35 -0700\r\n");
b.textContent(stringBuilder.toString());
Address author = new Address("Diffy", "test@gerritcodereview.com");
b.from(author);
- MailMetadata meta = MetadataParser.parse(b.build());
+ MailMetadata meta = MailHeaderParser.parse(b.build());
assertThat(meta.author).isEqualTo(author.getEmail());
assertThat(meta.changeNumber).isEqualTo(123);
assertThat(meta.patchSet).isEqualTo(1);
@@ -100,13 +98,12 @@
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(
- "<div id\"someid\">" + toFooterWithDelimiter(MetadataName.CHANGE_NUMBER) + "123</div>");
- stringBuilder.append("<div>" + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1</div>");
- stringBuilder.append(
- "<div>" + toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment</div>");
+ "<div id\"someid\">" + MailHeader.CHANGE_NUMBER.withDelimiter() + "123</div>");
+ stringBuilder.append("<div>" + MailHeader.PATCH_SET.withDelimiter() + "1</div>");
+ stringBuilder.append("<div>" + MailHeader.MESSAGE_TYPE.withDelimiter() + "comment</div>");
stringBuilder.append(
"<div>"
- + toFooterWithDelimiter(MetadataName.TIMESTAMP)
+ + MailHeader.COMMENT_DATE.withDelimiter()
+ "Tue, 25 Oct 2016 02:11:35 -0700"
+ "</div>");
b.htmlContent(stringBuilder.toString());
@@ -114,7 +111,7 @@
Address author = new Address("Diffy", "test@gerritcodereview.com");
b.from(author);
- MailMetadata meta = MetadataParser.parse(b.build());
+ MailMetadata meta = MailHeaderParser.parse(b.build());
assertThat(meta.author).isEqualTo(author.getEmail());
assertThat(meta.changeNumber).isEqualTo(123);
assertThat(meta.patchSet).isEqualTo(1);
diff --git a/javatests/com/google/gerrit/server/permissions/RefControlTest.java b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
index 0bccf8b..7295f37 100644
--- a/javatests/com/google/gerrit/server/permissions/RefControlTest.java
+++ b/javatests/com/google/gerrit/server/permissions/RefControlTest.java
@@ -27,6 +27,7 @@
import static com.google.gerrit.server.project.testing.Util.ADMIN;
import static com.google.gerrit.server.project.testing.Util.DEVS;
import static com.google.gerrit.server.project.testing.Util.allow;
+import static com.google.gerrit.server.project.testing.Util.allowExclusive;
import static com.google.gerrit.server.project.testing.Util.block;
import static com.google.gerrit.server.project.testing.Util.deny;
import static com.google.gerrit.server.project.testing.Util.doNotInherit;
@@ -201,6 +202,7 @@
@Inject private SingleVersionListener singleVersionListener;
@Inject private InMemoryDatabase schemaFactory;
@Inject private ThreadLocalRequestContext requestContext;
+ @Inject private DefaultRefFilter.Factory refFilterFactory;
@Before
public void setUp() throws Exception {
@@ -634,6 +636,16 @@
}
@Test
+ public void unblockRead_NotPossible() {
+ block(parent, READ, ANONYMOUS_USERS, "refs/*");
+ allow(parent, READ, ADMIN, "refs/*");
+ allow(local, READ, ANONYMOUS_USERS, "refs/*");
+ allow(local, READ, ADMIN, "refs/*");
+ ProjectControl u = user(local);
+ assertCannotRead("refs/heads/master", u);
+ }
+
+ @Test
public void unblockForceWithAllowNoForce_NotPossible() {
PermissionRule r = block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
r.setForce(true);
@@ -671,6 +683,36 @@
}
@Test
+ public void unblockVoteMoreSpecificRefWithExclusiveFlag() {
+ String perm = LABEL + "Code-Review";
+
+ block(local, perm, -1, 1, ANONYMOUS_USERS, "refs/heads/*");
+ allowExclusive(local, perm, -2, 2, DEVS, "refs/heads/master");
+
+ ProjectControl u = user(local, DEVS);
+ PermissionRange range = u.controlForRef("refs/heads/master").getRange(perm);
+ assertCanVote(-2, range);
+ }
+
+ @Test
+ public void unblockFromParentDoesNotAffectChild() {
+ allow(parent, PUSH, DEVS, "refs/heads/master", true);
+ block(local, PUSH, DEVS, "refs/heads/master");
+
+ ProjectControl u = user(local, DEVS);
+ assertCannotUpdate("refs/heads/master", u);
+ }
+
+ @Test
+ public void unblockFromParentDoesNotAffectChildDifferentGroups() {
+ allow(parent, PUSH, DEVS, "refs/heads/master", true);
+ block(local, PUSH, ANONYMOUS_USERS, "refs/heads/master");
+
+ ProjectControl u = user(local, DEVS);
+ assertCannotUpdate("refs/heads/master", u);
+ }
+
+ @Test
public void unblockMoreSpecificRefInLocalWithExclusiveFlag_Fails() {
block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*");
allow(local, PUSH, DEVS, "refs/heads/master", true);
@@ -680,6 +722,16 @@
}
@Test
+ public void blockMoreSpecificRefWithinProject() {
+ block(local, PUSH, ANONYMOUS_USERS, "refs/heads/secret");
+ allow(local, PUSH, DEVS, "refs/heads/*", true);
+
+ ProjectControl u = user(local, DEVS);
+ assertCannotUpdate("refs/heads/secret", u);
+ assertCanUpdate("refs/heads/master", u);
+ }
+
+ @Test
public void unblockOtherPermissionWithMoreSpecificRefAndExclusiveFlag_Fails() {
block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*");
allow(local, PUSH, DEVS, "refs/heads/master");
@@ -847,6 +899,18 @@
}
@Test
+ public void unionOfBlockedVotes() {
+ allow(parent, LABEL + "Code-Review", -1, +1, DEVS, "refs/heads/*");
+ block(parent, LABEL + "Code-Review", -2, +2, REGISTERED_USERS, "refs/heads/*");
+ block(local, LABEL + "Code-Review", -2, +1, REGISTERED_USERS, "refs/heads/*");
+
+ ProjectControl u = user(local, DEVS);
+ PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
+ assertCanVote(-1, range);
+ assertCannotVote(1, range);
+ }
+
+ @Test
public void blockOwner() {
block(parent, OWNER, ANONYMOUS_USERS, "refs/*");
allow(local, OWNER, DEVS, "refs/*");
@@ -919,6 +983,7 @@
sectionSorter,
changeControlFactory,
permissionBackend,
+ refFilterFactory,
new MockUser(name, memberOf),
newProjectState(local));
}
diff --git a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
index 267c622..e3aec45 100644
--- a/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
+++ b/javatests/com/google/gerrit/server/project/CommitsCollectionTest.java
@@ -19,118 +19,61 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import com.google.common.collect.ImmutableList;
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.lifecycle.LifecycleManager;
+import com.google.gerrit.common.data.PermissionRule;
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.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.testing.Util;
import com.google.gerrit.server.restapi.project.CommitsCollection;
-import com.google.gerrit.server.schema.SchemaCreator;
-import com.google.gerrit.server.util.RequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gerrit.testing.InMemoryDatabase;
-import com.google.gerrit.testing.InMemoryModule;
import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.inject.Guice;
+import com.google.gerrit.testing.InMemoryTestEnvironment;
import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Provider;
-import com.google.inject.util.Providers;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
-import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
/** Unit tests for {@link CommitsCollection}. */
public class CommitsCollectionTest {
+ @Rule public InMemoryTestEnvironment testEnvironment = new InMemoryTestEnvironment();
+
@Inject private AccountManager accountManager;
- @Inject private IdentifiedUser.GenericFactory userFactory;
- @Inject private InMemoryDatabase schemaFactory;
@Inject private InMemoryRepositoryManager repoManager;
- @Inject private SchemaCreator schemaCreator;
- @Inject private ThreadLocalRequestContext requestContext;
@Inject protected ProjectCache projectCache;
@Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
@Inject protected AllProjectsName allProjects;
- @Inject protected GroupCache groupCache;
@Inject private CommitsCollection commits;
- private LifecycleManager lifecycle;
- private ReviewDb db;
private TestRepository<InMemoryRepository> repo;
private ProjectConfig project;
- private IdentifiedUser user;
- private AccountGroup.UUID admins;
@Before
public void setUp() throws Exception {
- Injector injector = Guice.createInjector(new InMemoryModule());
- injector.injectMembers(this);
- lifecycle = new LifecycleManager();
- lifecycle.add(injector);
- lifecycle.start();
-
- db = schemaFactory.open();
- schemaCreator.create(db);
- // Need to create at least one user to be admin before creating a "normal"
- // registered user.
- // See AccountManager#create().
- accountManager.authenticate(AuthRequest.forUser("admin")).getAccountId();
- admins = groupCache.get(new AccountGroup.NameKey("Administrators")).orElse(null).getGroupUUID();
setUpPermissions();
- Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
- user = userFactory.create(userId);
+ Account.Id user = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
+ testEnvironment.setApiUser(user);
Project.NameKey name = new Project.NameKey("project");
InMemoryRepository inMemoryRepo = repoManager.createRepository(name);
project = new ProjectConfig(name);
project.load(inMemoryRepo);
repo = new TestRepository<>(inMemoryRepo);
-
- requestContext.setContext(
- new RequestContext() {
- @Override
- public CurrentUser getUser() {
- return user;
- }
-
- @Override
- public Provider<ReviewDb> getReviewDbProvider() {
- return Providers.of(db);
- }
- });
- }
-
- @After
- public void tearDown() {
- if (repo != null) {
- repo.getRepository().close();
- }
- if (lifecycle != null) {
- lifecycle.stop();
- }
- requestContext.setContext(null);
- if (db != null) {
- db.close();
- }
- InMemoryDatabase.drop(schemaFactory);
}
@Test
@@ -256,6 +199,8 @@
}
private void setUpPermissions() throws Exception {
+ ImmutableList<AccountGroup.UUID> admins = getAdmins();
+
// Remove read permissions for all users besides admin, because by default
// Anonymous user group has ALLOW READ permission in refs/*.
// This method is idempotent, so is safe to call on every test setup.
@@ -263,6 +208,24 @@
for (AccessSection sec : pc.getAccessSections()) {
sec.removePermission(Permission.READ);
}
- allow(pc, Permission.READ, admins, "refs/*");
+ for (AccountGroup.UUID admin : admins) {
+ allow(pc, Permission.READ, admin, "refs/*");
+ }
+ }
+
+ private ImmutableList<AccountGroup.UUID> getAdmins() {
+ Permission adminPermission =
+ projectCache
+ .getAllProjects()
+ .getConfig()
+ .getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
+ .getPermission(GlobalCapability.ADMINISTRATE_SERVER);
+
+ return adminPermission
+ .getRules()
+ .stream()
+ .map(PermissionRule::getGroup)
+ .map(GroupReference::getUUID)
+ .collect(ImmutableList.toImmutableList());
}
}
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index 1c124d2..2bff3f9 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -16,8 +16,8 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
import static java.util.stream.Collectors.toList;
-import static org.junit.Assert.fail;
import com.google.common.base.CharMatcher;
import com.google.gerrit.extensions.api.GerritApi;
@@ -27,7 +27,6 @@
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
@@ -263,12 +262,7 @@
@Test
public void byMember() throws Exception {
- if (getSchemaVersion() < 4) {
- assertMissingField(GroupField.MEMBER);
- assertFailingQuery(
- "member:someName", "'member' operator is not supported by group index version");
- return;
- }
+ assume().that(getSchemaVersion() >= 4).isTrue();
AccountInfo user1 = createAccount("user1", "User1", "user1@example.com");
AccountInfo user2 = createAccount("user2", "User2", "user2@example.com");
@@ -288,12 +282,7 @@
@Test
public void bySubgroups() throws Exception {
- if (getSchemaVersion() < 4) {
- assertMissingField(GroupField.SUBGROUP);
- assertFailingQuery(
- "subgroup:someGroupName", "'subgroup' operator is not supported by group index version");
- return;
- }
+ assume().that(getSchemaVersion() >= 4).isTrue();
GroupInfo superParentGroup = createGroup(name("superParentGroup"));
GroupInfo parentGroup1 = createGroup(name("parentGroup1"));
@@ -549,21 +538,6 @@
return name + "_" + getSanitizedMethodName();
}
- protected void assertMissingField(FieldDef<InternalGroup, ?> field) {
- assertThat(getSchema().hasField(field))
- .named("schema %s has field %s", getSchemaVersion(), field.getName())
- .isFalse();
- }
-
- protected void assertFailingQuery(String query, String expectedMessage) throws Exception {
- try {
- assertQuery(query);
- fail("expected BadRequestException for query '" + query + "'");
- } catch (BadRequestException e) {
- assertThat(e.getMessage()).isEqualTo(expectedMessage);
- }
- }
-
protected int getSchemaVersion() {
return getSchema().getVersion();
}
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index 2b235a1..67672d3 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -17,98 +17,37 @@
import static org.junit.Assert.assertEquals;
import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.reviewdb.client.Account;
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.IdentifiedUser;
-import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.server.schema.SchemaCreator;
-import com.google.gerrit.server.util.RequestContext;
-import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gerrit.testing.InMemoryDatabase;
-import com.google.gerrit.testing.InMemoryModule;
-import com.google.gerrit.testing.InMemoryRepositoryManager;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Guice;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.testing.InMemoryTestEnvironment;
import com.google.inject.Inject;
-import com.google.inject.Injector;
import com.google.inject.Provider;
-import com.google.inject.util.Providers;
-import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
-import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
public class BatchUpdateTest {
- @Inject private AccountManager accountManager;
- @Inject private IdentifiedUser.GenericFactory userFactory;
- @Inject private SchemaFactory<ReviewDb> schemaFactory;
- @Inject private InMemoryRepositoryManager repoManager;
- @Inject private SchemaCreator schemaCreator;
- @Inject private ThreadLocalRequestContext requestContext;
+ @Rule public InMemoryTestEnvironment testEnvironment = new InMemoryTestEnvironment();
+
+ @Inject private GitRepositoryManager repoManager;
@Inject private BatchUpdate.Factory batchUpdateFactory;
+ @Inject private ReviewDb db;
+ @Inject private Provider<CurrentUser> user;
- // Only for use in setting up/tearing down injector; other users should use schemaFactory.
- @Inject private InMemoryDatabase inMemoryDatabase;
-
- private LifecycleManager lifecycle;
- private ReviewDb db;
- private TestRepository<InMemoryRepository> repo;
private Project.NameKey project;
- private IdentifiedUser user;
+ private TestRepository<Repository> repo;
@Before
public void setUp() throws Exception {
- Injector injector = Guice.createInjector(new InMemoryModule());
- injector.injectMembers(this);
- lifecycle = new LifecycleManager();
- lifecycle.add(injector);
- lifecycle.start();
-
- try (ReviewDb underlyingDb = inMemoryDatabase.getDatabase().open()) {
- schemaCreator.create(underlyingDb);
- }
- db = schemaFactory.open();
- Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
- user = userFactory.create(userId);
-
project = new Project.NameKey("test");
- InMemoryRepository inMemoryRepo = repoManager.createRepository(project);
+ Repository inMemoryRepo = repoManager.createRepository(project);
repo = new TestRepository<>(inMemoryRepo);
-
- requestContext.setContext(
- new RequestContext() {
- @Override
- public CurrentUser getUser() {
- return user;
- }
-
- @Override
- public Provider<ReviewDb> getReviewDbProvider() {
- return Providers.of(db);
- }
- });
- }
-
- @After
- public void tearDown() {
- if (repo != null) {
- repo.getRepository().close();
- }
- if (lifecycle != null) {
- lifecycle.stop();
- }
- requestContext.setContext(null);
- if (db != null) {
- db.close();
- }
- InMemoryDatabase.drop(inMemoryDatabase);
}
@Test
@@ -116,7 +55,7 @@
final RevCommit masterCommit = repo.branch("master").commit().create();
final RevCommit branchCommit = repo.branch("branch").commit().parent(masterCommit).create();
- try (BatchUpdate bu = batchUpdateFactory.create(db, project, user, TimeUtil.nowTs())) {
+ try (BatchUpdate bu = batchUpdateFactory.create(db, project, user.get(), TimeUtil.nowTs())) {
bu.addRepoOnlyOp(
new RepoOnlyOp() {
@Override
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl
index c035793..5ee3535 100644
--- a/lib/js/bower_archives.bzl
+++ b/lib/js/bower_archives.bzl
@@ -25,8 +25,8 @@
bower_archive(
name = "font-roboto",
package = "PolymerElements/font-roboto",
- version = "1.0.3",
- sha1 = "edf478d20ae2fc0704d7c155e20162caaabdd5ae")
+ version = "1.1.0",
+ sha1 = "ab4218d87b9ce569d6282b01f7642e551879c3d5")
bower_archive(
name = "iron-a11y-announcer",
package = "PolymerElements/iron-a11y-announcer",
@@ -39,7 +39,7 @@
sha1 = "f58358ee652c67e6e721364ba50fb77a2ece1465")
bower_archive(
name = "iron-behaviors",
- package = "polymerelements/iron-behaviors",
+ package = "PolymerElements/iron-behaviors",
version = "1.0.18",
sha1 = "e231a1a02b090f5183db917639fdb96cdd0dca18")
bower_archive(
@@ -55,8 +55,8 @@
bower_archive(
name = "iron-flex-layout",
package = "PolymerElements/iron-flex-layout",
- version = "1.3.7",
- sha1 = "4d4cf3232cf750a17a7df0a37476117f831ac633")
+ version = "1.3.9",
+ sha1 = "d987b924cf29fcfe4b393833e81fdc9f1e268796")
bower_archive(
name = "iron-form-element-behavior",
package = "PolymerElements/iron-form-element-behavior",
@@ -103,6 +103,11 @@
version = "1.0.13",
sha1 = "a81eab28a952e124c208430e17508d9a1aae4ee7")
bower_archive(
+ name = "paper-icon-button",
+ package = "PolymerElements/paper-icon-button",
+ version = "2.1.0",
+ sha1 = "caead6a276877888d128ace809376980c3f3fe42")
+ bower_archive(
name = "paper-ripple",
package = "PolymerElements/paper-ripple",
version = "1.0.10",
diff --git a/lib/js/bower_components.bzl b/lib/js/bower_components.bzl
index fb40855..dc16ccf 100644
--- a/lib/js/bower_components.bzl
+++ b/lib/js/bower_components.bzl
@@ -229,6 +229,16 @@
seed = True,
)
bower_component(
+ name = "paper-icon-button",
+ license = "//lib:LICENSE-polymer",
+ deps = [
+ ":iron-icon",
+ ":paper-behaviors",
+ ":paper-styles",
+ ":polymer",
+ ],
+ )
+ bower_component(
name = "paper-input",
license = "//lib:LICENSE-polymer",
deps = [
@@ -282,6 +292,23 @@
],
)
bower_component(
+ name = "paper-tabs",
+ license = "//lib:LICENSE-polymer",
+ deps = [
+ ":iron-behaviors",
+ ":iron-flex-layout",
+ ":iron-icon",
+ ":iron-iconset-svg",
+ ":iron-menu-behavior",
+ ":iron-resizable-behavior",
+ ":paper-behaviors",
+ ":paper-icon-button",
+ ":paper-styles",
+ ":polymer",
+ ],
+ seed = True,
+ )
+ bower_component(
name = "paper-toggle-button",
license = "//lib:LICENSE-polymer",
deps = [
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index bac9b33..714fc10 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit bac9b336a326585accfc9a9eda7b5340e62783ac
+Subproject commit 714fc1057174227bd5acf4753039fe582c930e18
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator
index 3f96a82..315a115 160000
--- a/plugins/commit-message-length-validator
+++ b/plugins/commit-message-length-validator
@@ -1 +1 @@
-Subproject commit 3f96a82f15d9d592a86a989874b160567cd66f53
+Subproject commit 315a11558913fa8f9c6d3b1723e45583b25afa1c
diff --git a/plugins/replication b/plugins/replication
index 81a34e6..f57f029 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 81a34e608c97e9ac6aa7b2678fca96427e212413
+Subproject commit f57f0297e330660eb699d237cb587d7cc2c0e6b1
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index 223730f..4672856 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit 223730f10cf16f6a8ed2c0a3867371ca336d9ae7
+Subproject commit 467285664ebf8eb6f1e03ff13ebc706eee6d8662
diff --git a/plugins/singleusergroup b/plugins/singleusergroup
index 2c42f31..45003c4 160000
--- a/plugins/singleusergroup
+++ b/plugins/singleusergroup
@@ -1 +1 @@
-Subproject commit 2c42f316091726ba34a4f3809cb20197d8a46575
+Subproject commit 45003c4e290cd29a8695db438ca30302fa0683c7
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 0c2cd5e..7487ad5 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -26,6 +26,7 @@
"//lib/js:paper-input",
"//lib/js:paper-item",
"//lib/js:paper-listbox",
+ "//lib/js:paper-tabs",
"//lib/js:paper-toggle-button",
"//lib/js:polymer",
"//lib/js:polymer-resin",
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 991ce2e..593a34b 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -124,6 +124,7 @@
"change-list",
"core",
"diff",
+ "edit",
"plugins",
"settings",
"shared",
@@ -132,7 +133,7 @@
[sh_test(
name = "template_test_" + directory,
- size = "large",
+ size = "enormous",
srcs = ["template_test.sh"],
args = [directory],
data = [
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
index 0148377..d6d5a86 100644
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
@@ -25,12 +25,23 @@
/**
* @template T
* @param {!Array<T>} array
- * @param {!Function} fn
+ * @param {!Function} fn An iteratee function to be passed each element of
+ * the array in order. Must return a promise, and the following
+ * iteration will not begin until resolution of the promise returned by
+ * the previous iteration.
+ *
+ * An optional second argument to fn is a callback that will halt the
+ * loop if called.
* @return {!Promise<undefined>}
*/
asyncForeach(array, fn) {
if (!array.length) { return Promise.resolve(); }
- return fn(array[0]).then(() => this.asyncForeach(array.slice(1), fn));
+ let stop = false;
+ const stopCallback = () => { stop = true; };
+ return fn(array[0], stopCallback).then(exit => {
+ if (stop) { return Promise.resolve(); }
+ return this.asyncForeach(array.slice(1), fn);
+ });
},
};
})(window);
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
index ba15ad7..cbc7056 100644
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
@@ -35,5 +35,19 @@
assert.equal(fn.getCall(2).args[0], 3);
});
});
+
+ test('halts on stop condition', () => {
+ const stub = sinon.stub();
+ const fn = (e, stop) => {
+ stub(e);
+ stop();
+ return Promise.resolve();
+ };
+ return Gerrit.AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+ .then(() => {
+ assert.isTrue(stub.calledOnce);
+ assert.equal(stub.lastCall.args[0], 1);
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
index 19eb437..022d994 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -33,6 +33,7 @@
/** @polymerBehavior this */
Gerrit.PatchSetBehavior = {
EDIT_NAME: 'edit',
+ PARENT_NAME: 'PARENT',
/**
* As patchNum can be either a string (e.g. 'edit', 'PARENT') OR a number,
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
index f3db039..d3491c7 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
@@ -93,16 +93,20 @@
* Example
* // returns 'text.html'
* util.truncatePath.('text.html');
+ *
+ * @param {string} path
+ * @param {number=} opt_threshold
* @return {string} Returns the truncated value of a URL.
*/
- truncatePath(path) {
+ truncatePath(path, opt_threshold) {
+ const threshold = opt_threshold || 1;
const pathPieces = path.split('/');
- if (pathPieces.length < 2) {
- return path;
- }
+ if (pathPieces.length <= threshold) { return path; }
+
+ const index = pathPieces.length - threshold;
// Character is an ellipsis.
- return '\u2026/' + pathPieces[pathPieces.length - 1];
+ return `\u2026/${pathPieces.slice(index).join('/')}`;
},
};
})(window);
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
index e0b1b7e..f48fb98 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -66,6 +66,19 @@
assert.equal(shortenedPath, expectedPath);
});
+ test('truncatePath with opt_threshold', () => {
+ const truncatePath = Gerrit.PathListBehavior.truncatePath;
+ let path = 'level1/level2/level3/level4/file.js';
+ let shortenedPath = truncatePath(path, 2);
+ // The expected path is truncated with an ellipsis.
+ const expectedPath = '\u2026/level4/file.js';
+ assert.equal(shortenedPath, expectedPath);
+
+ path = 'level2/file.js';
+ shortenedPath = truncatePath(path, 2);
+ assert.equal(shortenedPath, path);
+ });
+
test('truncatePath with short path should not add ellipsis', () => {
const truncatePath = Gerrit.PathListBehavior.truncatePath;
const path = 'file.js';
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
index 2d84179..be5cd13 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
@@ -68,6 +68,7 @@
class="confirmDialog"
disabled="[[!_hasNewGroupName]]"
confirm-label="Create"
+ confirm-on-enter
on-confirm="_handleCreateGroup"
on-cancel="_handleCloseCreate">
<div class="header" slot="header">
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
index da20181..fdfce5a 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html
@@ -28,6 +28,7 @@
</style>
<gr-confirm-dialog
confirm-label="Delete [[_computeItemName(itemType)]]"
+ confirm-on-enter
on-confirm="_handleConfirmTap"
on-cancel="_handleCancelTap">
<div class="header" slot="header">[[_computeItemName(itemType)]] Deletion</div>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
index 2cfa0b2..8bceff1 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.html
@@ -35,19 +35,24 @@
:host {
display: inline-block;
}
- input {
- width: 25em;
+ input:not([type="checkbox"]),
+ gr-autocomplete,
+ iron-autogrow-textarea {
+ width: 100%;
+ }
+ .value {
+ width: 32em;
+ }
+ section {
+ align-items: center;
+ display: flex;
+ }
+ #description {
+ align-items: initial;
}
gr-autocomplete {
- border: none;
- float: right;
--gr-autocomplete: {
- border: 1px solid #d1d2d3;
- border-radius: 2px;
- font-size: var(--font-size-normal);
- height: 2em;
padding: 0 .15em;
- width: 20em;
}
}
</style>
@@ -65,40 +70,50 @@
</span>
</section>
<section>
- <span class="title">Enter topic for new change (optional)</span>
- <input
- is="iron-input"
- id="tagNameInput"
- maxlength="1024"
- bind-value="{{topic}}">
+ <span class="title">Enter topic for new change</span>
+ <span class="value">
+ <input
+ is="iron-input"
+ id="tagNameInput"
+ maxlength="1024"
+ placeholder="(optional)"
+ bind-value="{{topic}}">
+ </span>
</section>
- <section>
+ <section id="description">
<span class="title">Description</span>
- <iron-autogrow-textarea
- id="messageInput"
- class="message"
- autocomplete="on"
- rows="4"
- max-rows="15"
- bind-value="{{subject}}"
- placeholder="Insert the description of the change.">
- </iron-autogrow-textarea>
+ <span class="value">
+ <iron-autogrow-textarea
+ id="messageInput"
+ class="message"
+ autocomplete="on"
+ rows="4"
+ max-rows="15"
+ bind-value="{{subject}}"
+ placeholder="Insert the description of the change.">
+ </iron-autogrow-textarea>
+ </span>
</section>
<section>
- <span class="title">Options</span>
- <section>
- <label for="privateChangeCheckBox">Private Change</label>
+ <label
+ class="title"
+ for="privateChangeCheckBox">Private Change</label>
+ <span class="value">
<input
type="checkbox"
id="privateChangeCheckBox"
checked$="[[_repoConfig.private_by_default.inherited_value]]">
- </section>
- <section>
- <label for="wipChangeCheckBox">WIP Change</label>
+ </span>
+ </section>
+ <section>
+ <label
+ class="title"
+ for="wipChangeCheckBox">WIP Change</label>
+ <span class="value">
<input
type="checkbox"
id="wipChangeCheckBox">
- </section>
+ </span>
</section>
</div>
</div>
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 b16315e..6c78f7c 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
@@ -52,7 +52,7 @@
<div class="gr-form-styles">
<div id="form">
<section>
- <span class="title">Repositories name</span>
+ <span class="title">Repository name</span>
<input is="iron-input"
id="repoNameInput"
autocomplete="on"
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index 1ce05b1..5257e69 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -15,6 +15,8 @@
'use strict';
const SUGGESTIONS_LIMIT = 15;
+ const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+
+ 'permission to add it';
const URL_REGEX = '^(?:[a-z]+:)?//';
@@ -186,7 +188,16 @@
_handleSavingIncludedGroups() {
return this.$.restAPI.saveIncludedGroup(this._groupName,
- this._includedGroupSearch)
+ this._includedGroupSearch, err => {
+ if (err.status === 404) {
+ this.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {message: SAVING_ERROR_TEXT},
+ bubbles: true,
+ }));
+ return err;
+ }
+ throw Error(err.statusText);
+ })
.then(config => {
if (!config) {
return;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index 194750e..d670d4d 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -183,6 +183,24 @@
});
});
+ test('add included group 404 shows helpful error text', () => {
+ element._groupOwner = true;
+
+ const memberName = 'bad-name';
+ const alertStub = sandbox.stub();
+ element.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(element.$.restAPI, 'saveGroupMembers',
+ () => Promise.reject({status: 404}));
+
+ element.$.groupMemberSearchInput.text = memberName;
+ element.$.groupMemberSearchInput.value = 1234;
+
+ return element._handleSavingIncludedGroups().then(() => {
+ assert.isTrue(alertStub.called);
+ });
+ });
+
test('_getAccountSuggestions empty', () => {
return element._getAccountSuggestions('nonexistent').then(accounts => {
assert.equal(accounts.length, 0);
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 77dfad8..e650023 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
@@ -47,9 +47,18 @@
display: inline-block;
margin: 1em 0;
}
+ .weblink {
+ margin-right: .2em;
+ }
+ .weblinks {
+ display: none;
+ }
+ .weblinks.show {
+ display: block;
+ }
</style>
<style include="gr-menu-page-styles"></style>
- <main class$="[[_computeAdminClass(_isAdmin)]]">
+ <main class$="[[_computeAdminClass(_isAdmin, _canUpload)]]">
<div class="gwtLink">Editing access in the new UI is a work in progress. Visit the
<a href$="[[computeGwtUrl(path)]]" rel="external">Old UI</a>
if you need a feature that is not yet supported.
@@ -60,6 +69,14 @@
[[_inheritsFrom.name]]</a>
</h3>
</template>
+ <div class$="weblinks [[_computeWebLinkClass(_weblinks)]]">
+ History:
+ <template is="dom-repeat" items="[[_weblinks]]" as="link">
+ <a href="[[link.url]]" class="weblink" rel="noopener" target="[[link.target]]">
+ [[link.name]]
+ </a>
+ </template>
+ </div>
<gr-button id="editBtn"
class$="[[_computeShowEditClass(_sections)]]"
on-tap="_handleEdit">[[_editOrCancel(_editing)]]</gr-button>
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 65045db..7149292 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
@@ -78,6 +78,11 @@
type: Boolean,
value: false,
},
+ _canUpload: {
+ type: Boolean,
+ value: false,
+ },
+ _ownerOf: Array,
_capabilities: Object,
_groups: Object,
/** @type {?} */
@@ -94,6 +99,7 @@
value: false,
},
_sections: Array,
+ _weblinks: Array,
},
behaviors: [
@@ -123,6 +129,8 @@
this._inheritsFrom = res.inherits_from;
this._local = res.local;
this._groups = res.groups;
+ this._weblinks = res.config_web_links || [];
+ this._canUpload = res.can_upload;
return this.toSortedArray(this._local);
}));
@@ -153,6 +161,10 @@
return editing ? 'Cancel' : 'Edit';
},
+ _computeWebLinkClass(weblinks) {
+ return weblinks.length ? 'show' : '';
+ },
+
_handleEditingChanged(editing, editingOld) {
// Ignore when editing gets set initially.
if (!editingOld || editing) { return; }
@@ -305,8 +317,8 @@
return 'visible';
},
- _computeAdminClass(isAdmin) {
- return isAdmin ? 'admin' : '';
+ _computeAdminClass(isAdmin, canUpload) {
+ return isAdmin || canUpload ? 'admin' : '';
},
_computeParentHref(repoName) {
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 dece995..b5efbe3 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
@@ -64,6 +64,12 @@
name: 'Maintainers',
},
},
+ config_web_links: [{
+ name: 'gitiles',
+ target: '_blank',
+ url: 'https://my/site/+log/123/project.config',
+ }],
+ can_upload: true,
};
const accessRes2 = {
local: {
@@ -147,11 +153,15 @@
assert.deepEqual(element._sections,
element.toSortedArray(accessRes.local));
assert.deepEqual(element._labels, repoRes.labels);
+ assert.equal(getComputedStyle(element.$$('.weblinks')).display,
+ 'block');
return element._repoChanged('Another New Repo');
})
.then(() => {
assert.deepEqual(element._sections,
element.toSortedArray(accessRes2.local));
+ assert.equal(getComputedStyle(element.$$('.weblinks')).display,
+ 'none');
done();
});
});
@@ -202,25 +212,7 @@
});
suite('with defined sections', () => {
- setup(() => {
- // Create deep copies of these objects so the originals are not modified
- // by any tests.
- element._local = JSON.parse(JSON.stringify(accessRes.local));
- element._sections = element.toSortedArray(element._local);
- element._groups = JSON.parse(JSON.stringify(accessRes.groups));
- element._capabilities = JSON.parse(JSON.stringify(capabilitiesRes));
- element._labels = JSON.parse(JSON.stringify(repoRes.labels));
- flushAsynchronousOperations();
- });
-
- test('button visibility for non admin', () => {
- assert.equal(getComputedStyle(element.$.saveBtn).display, 'none');
- assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
- });
-
- test('button visibility for admin', () => {
- element._isAdmin = true;
-
+ const testEditSaveCancelBtns = () => {
// Edit button is visible and Save button is hidden.
assert.equal(getComputedStyle(element.$.saveBtn).display, 'none');
assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none');
@@ -237,6 +229,32 @@
// Save button should be enabled after access is modified
element.fire('access-modified');
assert.isFalse(element.$.saveBtn.disabled);
+ };
+
+ setup(() => {
+ // Create deep copies of these objects so the originals are not modified
+ // by any tests.
+ element._local = JSON.parse(JSON.stringify(accessRes.local));
+ element._sections = element.toSortedArray(element._local);
+ element._groups = JSON.parse(JSON.stringify(accessRes.groups));
+ element._capabilities = JSON.parse(JSON.stringify(capabilitiesRes));
+ element._labels = JSON.parse(JSON.stringify(repoRes.labels));
+ flushAsynchronousOperations();
+ });
+
+ test('button visibility for non admin', () => {
+ assert.equal(getComputedStyle(element.$.saveBtn).display, 'none');
+ assert.equal(getComputedStyle(element.$.editBtn).display, 'none');
+ });
+
+ test('button visibility for non admin with upload privilege', () => {
+ element._canUpload = true;
+ testEditSaveCancelBtns();
+ });
+
+ test('button visibility for admin', () => {
+ element._isAdmin = true;
+ testEditSaveCancelBtns();
});
test('_handleAccessModified called with event fired', () => {
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 7fb5fe4..0331ff2 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
@@ -15,6 +15,7 @@
-->
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html">
+<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
@@ -39,8 +40,7 @@
background-color: #f5fafd;
}
:host([selected]) {
- border: 1px solid #2a66d9;
- border-left-width: .5rem;
+ border-left: .5rem solid #2a66d9;
}
:host([needs-review]) {
font-family: var(--font-family-bold);
@@ -100,13 +100,15 @@
.u-gray-background {
background-color: #F5F5F5;
}
+ .placeholder {
+ color: rgba(0, 0, 0, .87);
+ }
@media only screen and (max-width: 50em) {
:host {
display: flex;
}
:host([selected]) {
- border: none;
- border-top: 1px solid #ddd;
+ border-left: none;
}
}
</style>
@@ -116,7 +118,7 @@
<gr-change-star change="{{change}}"></gr-change-star>
</td>
<td class="cell number" hidden$="[[!showNumber]]" hidden>
- <a href$="[[changeURL]]"> [[change._number]]</a>
+ <a href$="[[changeURL]]">[[change._number]]</a>
</td>
<td class="cell subject"
hidden$="[[isColumnHidden('Subject', visibleChangeTableColumns)]]">
@@ -134,21 +136,38 @@
</td>
<td class="cell status"
hidden$="[[isColumnHidden('Status', visibleChangeTableColumns)]]">
- [[changeStatusString(change)]]
+ <template is="dom-if" if="[[status]]">
+ [[status]]
+ </template>
+ <template is="dom-if" if="[[!status]]">
+ <span class="placeholder">--</span>
+ </template>
</td>
<td class="cell owner"
hidden$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]">
- <gr-account-link account="[[change.owner]]"></gr-account-link>
+ <gr-account-link
+ account="[[change.owner]]"
+ additional-text="[[_computeAccountStatusString(change.owner)]]"></gr-account-link>
</td>
<td class="cell assignee"
hidden$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]">
<template is="dom-if" if="[[change.assignee]]">
- <gr-account-link account="[[change.assignee]]"></gr-account-link>
+ <gr-account-link
+ account="[[change.assignee]]"
+ additional-text="[[_computeAccountStatusString(change.owner)]]"></gr-account-link>
+ </template>
+ <template is="dom-if" if="[[!change.assignee]]">
+ <span class="placeholder">--</span>
</template>
</td>
<td class="cell project"
hidden$="[[isColumnHidden('Project', visibleChangeTableColumns)]]">
- <a href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a>
+ <a class="fullProject" href$="[[_computeProjectURL(change.project)]]">
+ [[change.project]]
+ </a>
+ <a class="truncatedProject" href$="[[_computeProjectURL(change.project)]]">
+ [[_computeTruncatedProject(change.project)]]
+ </a>
</td>
<td class="cell branch"
hidden$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]">
@@ -157,7 +176,7 @@
</a>
<template is="dom-if" if="[[change.topic]]">
(<a href$="[[_computeTopicURL(change)]]"><!--
- --><gr-limited-text limit="30" text="[[change.topic]]">
+ --><gr-limited-text limit="50" text="[[change.topic]]">
</gr-limited-text><!--
--></a>)
</template>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index aa40a6e..b029d46 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -29,6 +29,10 @@
type: String,
computed: '_computeChangeURL(change)',
},
+ status: {
+ type: String,
+ computed: 'changeStatusString(change)',
+ },
showStar: {
type: Boolean,
value: false,
@@ -39,6 +43,7 @@
behaviors: [
Gerrit.BaseUrlBehavior,
Gerrit.ChangeTableBehavior,
+ Gerrit.PathListBehavior,
Gerrit.RESTClientBehavior,
Gerrit.URLEncodingBehavior,
],
@@ -115,5 +120,14 @@
if (!change.topic) { return ''; }
return Gerrit.Nav.getUrlForTopic(change.topic);
},
+
+ _computeTruncatedProject(project) {
+ if (!project) { return ''; }
+ return this.truncatePath(project, 2);
+ },
+
+ _computeAccountStatusString(account) {
+ return account && account.status ? `(${account.status})` : '';
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 243a8ab..5e44a45 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -188,7 +188,10 @@
});
test('assignee only displayed if there is one', () => {
+ element.change = {};
+ flushAsynchronousOperations();
assert.isNotOk(element.$$('.assignee gr-account-link'));
+ assert.equal(element.$$('.assignee').textContent.trim(), '--');
element.change = {
assignee: {
name: 'test',
@@ -197,5 +200,11 @@
flushAsynchronousOperations();
assert.isOk(element.$$('.assignee gr-account-link'));
});
+
+ test('_computeAccountStatusString', () => {
+ assert.equal(element._computeAccountStatusString({}), '');
+ assert.equal(element._computeAccountStatusString({status: 'Working'}),
+ '(Working)');
+ });
});
</script>
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 276febb..f2c7a48 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
@@ -18,6 +18,7 @@
<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+<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-user-header/gr-user-header.html">
@@ -37,15 +38,25 @@
gr-change-list {
width: 100%;
}
+ gr-user-header {
+ border-bottom: 1px solid #ddd;
+ }
nav {
- padding: .5em 0;
- text-align: center;
+ align-items: center;
+ background-color: var(--view-background-color);;
+ display: flex;
+ height: 3rem;
+ justify-content: flex-end;
+ margin-right: 20px;
}
- nav a {
- display: inline-block;
+ nav,
+ iron-icon {
+ color: rgba(0, 0, 0, .87);
}
- nav a:first-of-type {
- margin-right: .5em;
+ iron-icon {
+ height: 1.85rem;
+ margin-left: 16px;
+ width: 1.85rem;
}
.hide {
display: none;
@@ -68,14 +79,18 @@
changes="{{_changes}}"
selected-index="{{viewState.selectedChangeIndex}}"
show-star="[[loggedIn]]"></gr-change-list>
- <nav>
- <a id="prevArrow"
- href$="[[_computeNavLink(_query, _offset, -1, _changesPerPage)]]"
- hidden$="[[_hidePrevArrow(_offset)]]" hidden>← Prev</a>
- <a id="nextArrow"
- href$="[[_computeNavLink(_query, _offset, 1, _changesPerPage)]]"
- hidden$="[[_hideNextArrow(_loading)]]" hidden>
- Next →</a>
+ <nav class$="[[_computeNavClass(_loading)]]">
+ Page [[_computePage(_offset, _changesPerPage)]]
+ <a id="prevArrow"
+ href$="[[_computeNavLink(_query, _offset, -1, _changesPerPage)]]"
+ class$="[[_computePrevArrowClass(_offset)]]">
+ <iron-icon icon="gr-icons:chevron-left"></iron-icon>
+ </a>
+ <a id="nextArrow"
+ href$="[[_computeNavLink(_query, _offset, 1, _changesPerPage)]]"
+ class$="[[_computeNextArrowClass(_changes)]]">
+ <iron-icon icon="gr-icons:chevron-right"></iron-icon>
+ </a>
</nav>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
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 c48b860..890713a 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
@@ -176,21 +176,20 @@
offset = +(offset || 0);
const limit = this._limitFor(query, changesPerPage);
const newOffset = Math.max(0, offset + (limit * direction));
- // Double encode URI component.
- let href = this.getBaseUrl() + '/q/' + this.encodeURL(query, false);
- if (newOffset > 0) {
- href += ',' + newOffset;
- }
- return href;
+ return Gerrit.Nav.getUrlForSearchQuery(query, newOffset);
},
- _hidePrevArrow(offset) {
- return offset === 0;
+ _computePrevArrowClass(offset) {
+ return offset === 0 ? 'hide' : '';
},
- _hideNextArrow(loading) {
- return loading || !this._changes || !this._changes.length ||
- !this._changes[this._changes.length - 1]._more_changes;
+ _computeNextArrowClass(changes) {
+ const more = changes.length && changes[changes.length - 1]._more_changes;
+ return more ? '' : 'hide';
+ },
+
+ _computeNavClass(loading) {
+ return loading || !this._changes || !this._changes.length ? 'hide' : '';
},
_handleNextPage() {
@@ -217,5 +216,9 @@
_computeUserHeaderClass(userId) {
return userId ? '' : 'hide';
},
+
+ _computePage(offset, changesPerPage) {
+ return offset / changesPerPage + 1;
+ },
});
})();
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 680f835..c0b1b81 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
@@ -46,6 +46,8 @@
getChanges(num, query) {
return Promise.resolve([]);
},
+ getAccountDetails() { return Promise.resolve({}); },
+ getAccountStatus() { return Promise.resolve({}); },
});
element = fixture('basic');
sandbox = sinon.sandbox.create();
@@ -58,15 +60,9 @@
});
});
- test('url is properly encoded', () => {
- assert.equal(element._computeNavLink(
- 'status:open project:platform/frameworks/base', 0, -1, 25),
- '/q/status:open+project:platform%252Fframeworks%252Fbase'
- );
- assert.equal(element._computeNavLink(
- 'status:open project:platform/frameworks/base', 0, 1, 25),
- '/q/status:open+project:platform%252Fframeworks%252Fbase,25'
- );
+ test('_computePage', () => {
+ assert.equal(element._computePage(0, 25), 1);
+ assert.equal(element._computePage(50, 25), 3);
});
test('_limitFor', () => {
@@ -79,74 +75,49 @@
});
test('_computeNavLink', () => {
+ const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForSearchQuery')
+ .returns('');
const query = 'status:open';
let offset = 0;
let direction = 1;
const changesPerPage = 5;
- assert.equal(
- element._computeNavLink(query, offset, direction, changesPerPage),
- '/q/status:open,5');
+
+ element._computeNavLink(query, offset, direction, changesPerPage);
+ assert.equal(getUrlStub.lastCall.args[1], 5);
+
direction = -1;
- assert.equal(
- element._computeNavLink(query, offset, direction, changesPerPage),
- '/q/status:open');
+ element._computeNavLink(query, offset, direction, changesPerPage);
+ assert.equal(getUrlStub.lastCall.args[1], 0);
+
offset = 5;
direction = 1;
- assert.equal(
- element._computeNavLink(query, offset, direction, changesPerPage),
- '/q/status:open,10');
- assert.equal(
- element._computeNavLink(
- query + ' limit:10', offset, direction, changesPerPage),
- '/q/status:open+limit:10,15');
+ element._computeNavLink(query, offset, direction, changesPerPage);
+ assert.equal(getUrlStub.lastCall.args[1], 10);
});
- test('_computeNavLink with path', () => {
- const oldCanonicalPath = window.CANONICAL_PATH;
- window.CANONICAL_PATH = '/r';
- const query = 'status:open';
+ test('_computePrevArrowClass', () => {
let offset = 0;
- let direction = 1;
- const changesPerPage = 5;
- assert.equal(
- element._computeNavLink(query, offset, direction, changesPerPage),
- '/r/q/status:open,5');
- direction = -1;
- assert.equal(
- element._computeNavLink(query, offset, direction, changesPerPage),
- '/r/q/status:open');
+ assert.equal(element._computePrevArrowClass(offset), 'hide');
offset = 5;
- direction = 1;
- assert.equal(
- element._computeNavLink(query, offset, direction, changesPerPage),
- '/r/q/status:open,10');
- window.CANONICAL_PATH = oldCanonicalPath;
+ assert.equal(element._computePrevArrowClass(offset), '');
});
- test('_hidePrevArrow', () => {
- let offset = 0;
- assert.isTrue(element._hidePrevArrow(offset));
- offset = 5;
- assert.isFalse(element._hidePrevArrow(offset));
+ test('_computeNextArrowClass', () => {
+ let changes = _.times(25, _.constant({_more_changes: true}));
+ assert.equal(element._computeNextArrowClass(changes), '');
+ changes = _.times(25, _.constant({}));
+ assert.equal(element._computeNextArrowClass(changes), 'hide');
});
- test('_hideNextArrow', () => {
+ test('_computeNavClass', () => {
let loading = true;
- assert.isTrue(element._hideNextArrow(loading));
+ assert.equal(element._computeNavClass(loading), 'hide');
loading = false;
- assert.isTrue(element._hideNextArrow(loading));
+ assert.equal(element._computeNavClass(loading), 'hide');
element._changes = [];
- assert.isTrue(element._hideNextArrow(loading));
- element._changes =
- Array(...Array(5)).map(Object.prototype.valueOf, {});
- assert.isTrue(element._hideNextArrow(loading));
- element._changes =
- Array(...Array(25)).map(Object.prototype.valueOf,
- {_more_changes: true});
- assert.isFalse(element._hideNextArrow(loading));
- element._changes =
- Array(...Array(25)).map(Object.prototype.valueOf, {});
- assert.isTrue(element._hideNextArrow(loading));
+ assert.equal(element._computeNavClass(loading), 'hide');
+ element._changes = _.times(5, _.constant({}));
+ assert.equal(element._computeNavClass(loading), '');
});
test('_handleNextPage', () => {
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 c8f606b..e129cee 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
@@ -38,6 +38,9 @@
.hide {
display: none;
}
+ gr-user-header {
+ border-bottom: 1px solid #ddd;
+ }
@media only screen and (max-width: 50em) {
.loading {
padding: 0 var(--default-horizontal-margin);
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 6acd9fa..7c04c1f 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
@@ -43,6 +43,12 @@
'(reviewer:${user} OR assignee:${user})',
},
{
+ // Open changes the viewed user is CCed on. Changes ignored by the viewing
+ // user are filtered out.
+ name: 'CCed on',
+ query: 'is:open -is:ignored cc:${user}',
+ },
+ {
name: 'Recently closed',
// Closed changes where viewed user is owner, reviewer, or assignee.
// Changes ignored by the viewing user are filtered out, and so are WIP
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 f7d933a..5621f9c 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
@@ -38,6 +38,11 @@
let paramsChangedPromise;
setup(() => {
+ stub('gr-rest-api-interface', {
+ getLoggedIn() { return Promise.resolve(false); },
+ getAccountDetails() { return Promise.resolve({}); },
+ getAccountStatus() { return Promise.resolve(false); },
+ });
element = fixture('basic');
sandbox = sinon.sandbox.create();
getChangesStub = sandbox.stub(element.$.restAPI, 'getChanges',
@@ -234,8 +239,8 @@
});
test('_computeUserHeaderClass', () => {
- assert.equal(element._computeUserHeaderClass(undefined), 'hide');
- assert.equal(element._computeUserHeaderClass(''), 'hide');
+ assert.equal(element._computeUserHeaderClass(undefined), '');
+ assert.equal(element._computeUserHeaderClass(''), '');
assert.equal(element._computeUserHeaderClass('self'), 'hide');
assert.equal(element._computeUserHeaderClass('user'), '');
});
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 5ac5b6d..322de71 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
@@ -68,9 +68,6 @@
/* px because don't have the same font size */
margin: 0 0 6px 0;
}
- .confirmDialog {
- width: 90vw;
- }
#actionLoadingMessage {
display: block;
margin: .5em;
@@ -167,6 +164,7 @@
id="confirmDeleteDialog"
class="confirmDialog"
confirm-label="Delete"
+ confirm-on-enter
on-cancel="_handleConfirmDialogCancel"
on-confirm="_handleDeleteConfirm">
<div class="header" slot="header">
@@ -180,6 +178,7 @@
id="confirmDeleteEditDialog"
class="confirmDialog"
confirm-label="Delete"
+ confirm-on-enter
on-cancel="_handleConfirmDialogCancel"
on-confirm="_handleDeleteEditConfirm">
<div class="header" slot="header">
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 6854f7a..a954559 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
@@ -52,7 +52,6 @@
ABANDON: 'abandon',
DELETE: '/',
DELETE_EDIT: 'deleteEdit',
- DONE_EDIT: 'doneEdit',
EDIT: 'edit',
IGNORE: 'ignore',
MOVE: 'move',
@@ -63,6 +62,7 @@
RESTORE: 'restore',
REVERT: 'revert',
REVIEWED: 'reviewed',
+ STOP_EDIT: 'stopEdit',
UNIGNORE: 'unignore',
UNREVIEWED: 'unreviewed',
WIP: 'wip',
@@ -159,11 +159,11 @@
__type: 'change',
};
- const DONE_EDIT = {
+ const STOP_EDIT = {
enabled: true,
- label: 'Done Editing',
+ label: 'Stop editing',
title: 'Stop editing this change',
- __key: 'doneEdit',
+ __key: 'stopEdit',
__primary: false,
__type: 'change',
};
@@ -569,14 +569,19 @@
delete this.actions.edit;
this.notifyPath('actions.edit');
}
- if (!changeActions.doneEdit) {
- this.set('actions.doneEdit', DONE_EDIT);
- }
} else {
if (!changeActions.edit) { this.set('actions.edit', EDIT); }
- if (changeActions.doneEdit) {
- delete this.actions.doneEdit;
- this.notifyPath('actions.doneEdit');
+ }
+ // Only show STOP_EDIT if edit mode is enabled, but no edit patch set
+ // is loaded.
+ if (editMode && !editPatchsetLoaded) {
+ if (!changeActions.stopEdit) {
+ this.set('actions.stopEdit', STOP_EDIT);
+ }
+ } else {
+ if (changeActions.stopEdit) {
+ delete this.actions.stopEdit;
+ this.notifyPath('actions.stopEdit');
}
}
}
@@ -831,8 +836,8 @@
case ChangeActions.EDIT:
this._handleEditTap();
break;
- case ChangeActions.DONE_EDIT:
- this._handleDoneEditTap();
+ case ChangeActions.STOP_EDIT:
+ this._handleStopEditTap();
break;
case ChangeActions.DELETE:
this._handleDeleteTap();
@@ -1316,8 +1321,8 @@
this.dispatchEvent(new CustomEvent('edit-tap', {bubbles: false}));
},
- _handleDoneEditTap() {
- this.dispatchEvent(new CustomEvent('done-edit-tap', {bubbles: false}));
+ _handleStopEditTap() {
+ this.dispatchEvent(new CustomEvent('stop-edit-tap', {bubbles: false}));
},
});
})();
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index e76e494..8986816 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -120,7 +120,7 @@
assert.isFalse(element._shouldHideActions({base: ['test']}, false));
});
- test('plugin revision actions', () => {
+ test('plugin revision actions', done => {
sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
Promise.resolve('the-url'));
element.revisionActions = {
@@ -131,10 +131,11 @@
assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith(
element.changeNum, element.latestPatchNum, '/plugin~action'));
assert.equal(element.revisionActions['plugin~action'].__url, 'the-url');
+ done();
});
});
- test('plugin change actions', () => {
+ test('plugin change actions', done => {
sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns(
Promise.resolve('the-url'));
element.actions = {
@@ -144,7 +145,8 @@
flush(() => {
assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith(
element.changeNum, null, '/plugin~action'));
- assert.equal(element.revisionActions['plugin~action'].__url, 'the-url');
+ assert.equal(element.actions['plugin~action'].__url, 'the-url');
+ done();
});
});
@@ -437,7 +439,7 @@
assert.isOk(element.$$('gr-button[data-action-key="rebaseEdit"]'));
assert.isOk(element.$$('gr-button[data-action-key="deleteEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="edit"]'));
- assert.isOk(element.$$('gr-button[data-action-key="doneEdit"]'));
+ assert.isNotOk(element.$$('gr-button[data-action-key="stopEdit"]'));
});
test('edit patchset is loaded, does not need rebase', () => {
@@ -451,7 +453,7 @@
assert.isNotOk(element.$$('gr-button[data-action-key="rebaseEdit"]'));
assert.isOk(element.$$('gr-button[data-action-key="deleteEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="edit"]'));
- assert.isOk(element.$$('gr-button[data-action-key="doneEdit"]'));
+ assert.isNotOk(element.$$('gr-button[data-action-key="stopEdit"]'));
});
test('edit mode is loaded, no edit patchset', () => {
@@ -464,7 +466,7 @@
assert.isNotOk(element.$$('gr-button[data-action-key="rebaseEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="deleteEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="edit"]'));
- assert.isOk(element.$$('gr-button[data-action-key="doneEdit"]'));
+ assert.isOk(element.$$('gr-button[data-action-key="stopEdit"]'));
});
test('normal patch set', () => {
@@ -477,7 +479,7 @@
assert.isNotOk(element.$$('gr-button[data-action-key="rebaseEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="deleteEdit"]'));
assert.isOk(element.$$('gr-button[data-action-key="edit"]'));
- assert.isNotOk(element.$$('gr-button[data-action-key="doneEdit"]'));
+ assert.isNotOk(element.$$('gr-button[data-action-key="stopEdit"]'));
});
test('edit action', done => {
@@ -487,7 +489,7 @@
flushAsynchronousOperations();
assert.isNotOk(element.$$('gr-button[data-action-key="edit"]'));
- assert.isOk(element.$$('gr-button[data-action-key="doneEdit"]'));
+ assert.isOk(element.$$('gr-button[data-action-key="stopEdit"]'));
element.set('editMode', false);
flushAsynchronousOperations();
@@ -629,6 +631,74 @@
assert.notInclude(element._disabledMenuActions, 'cherrypick');
});
+ suite('abandon change', () => {
+ let alertStub;
+ let fireActionStub;
+
+ setup(() => {
+ fireActionStub = sandbox.stub(element, '_fireAction');
+ alertStub = sandbox.stub(window, 'alert');
+ element.actions = {
+ abandon: {
+ method: 'POST',
+ label: 'Abandon',
+ title: 'Abandon the change',
+ enabled: true,
+ },
+ };
+ return element.reload();
+ });
+
+ test('abandon change with message', done => {
+ const newAbandonMsg = 'Test Abandon Message';
+ element.$.confirmAbandonDialog.message = newAbandonMsg;
+ flush(() => {
+ const abandonButton =
+ element.$$('gr-button[data-action-key="abandon"]');
+ MockInteractions.tap(abandonButton);
+
+ assert.equal(element.$.confirmAbandonDialog.message, newAbandonMsg);
+ done();
+ });
+ });
+
+ test('abandon change with no message', done => {
+ flush(() => {
+ const abandonButton =
+ element.$$('gr-button[data-action-key="abandon"]');
+ MockInteractions.tap(abandonButton);
+
+ assert.isUndefined(element.$.confirmAbandonDialog.message);
+ done();
+ });
+ });
+
+ test('works', () => {
+ element.$.confirmAbandonDialog.message = 'original message';
+ const restoreButton =
+ element.$$('gr-button[data-action-key="abandon"]');
+ MockInteractions.tap(restoreButton);
+
+ element.$.confirmAbandonDialog.message = 'foo message';
+ element._handleAbandonDialogConfirm();
+ assert.notOk(alertStub.called);
+
+ const action = {
+ __key: 'abandon',
+ __type: 'change',
+ __primary: false,
+ enabled: true,
+ label: 'Abandon',
+ method: 'POST',
+ title: 'Abandon the change',
+ };
+ assert.deepEqual(fireActionStub.lastCall.args, [
+ '/abandon', action, false, {
+ message: 'foo message',
+ }]);
+ });
+ });
+
suite('revert change', () => {
let alertStub;
let fireActionStub;
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 58972fd..be1ee6e 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
@@ -63,6 +63,9 @@
padding: .55em var(--default-horizontal-margin);
z-index: 99; /* Less than gr-overlay's backdrop */
}
+ .header.editMode {
+ background-color: #ebf5fb;
+ }
.header .download {
margin-right: 1em;
}
@@ -81,13 +84,13 @@
gr-change-status:first-child {
margin-left: 0;
}
- .header-title {
+ .headerTitle {
align-items: center;
display: flex;
flex: 1;
font-size: 1.2rem;
}
- .header-title .headerSubject {
+ .headerTitle .headerSubject {
font-family: var(--font-family-bold);
}
.replyContainer {
@@ -147,7 +150,6 @@
}
}
.changeStatuses,
- .changeText,
.commitActions,
.statusText {
align-items: center;
@@ -268,7 +270,7 @@
gr-change-star {
vertical-align: middle;
}
- .header-title {
+ .headerTitle {
flex-wrap: wrap;
font-size: 1.1rem;
}
@@ -321,8 +323,8 @@
id="mainContent"
class="container"
hidden$="{{_loading}}">
- <div class$="[[_computeHeaderClass(_change)]]">
- <div class="header-title">
+ <div class$="[[_computeHeaderClass(_editMode)]]">
+ <div class="headerTitle">
<gr-change-star
id="changeStar"
change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
@@ -345,12 +347,11 @@
</template>
</div>
<span class="separator"></span>
- <div class="changeText">
- <a aria-label$="[[_computeChangePermalinkAriaLabel(_change._number)]]"
- href$="[[_computeChangeUrl(_change)]]">[[_change._number]]</a>
- <span class="headerSubject">: [[_change.subject]]</span>
- </div>
- </div><!-- end header-title -->
+ <a aria-label$="[[_computeChangePermalinkAriaLabel(_change._number)]]"
+ href$="[[_computeChangeUrl(_change)]]">[[_change._number]]</a>
+ <pre>: </pre>
+ <span class="headerSubject">[[_change.subject]]</span>
+ </div><!-- end headerTitle -->
<div class="commitActions" hidden$="[[!_loggedIn]]">
<gr-change-actions id="actions"
change="[[_change]]"
@@ -369,7 +370,7 @@
edit-based-on-current-patch-set="[[hasEditBasedOnCurrentPatchSet(_allPatchSets)]]"
on-reload-change="_handleReloadChange"
on-edit-tap="_handleEditTap"
- on-done-edit-tap="_handleDoneEditTap"
+ on-stop-edit-tap="_handleStopEditTap"
on-download-tap="_handleOpenDownloadDialog"></gr-change-actions>
</div><!-- end commit actions -->
</div><!-- end header -->
@@ -469,7 +470,7 @@
</div>
</div>
</section>
- <section class="patchInfo hideOnMobileOverlay">
+ <section class="patchInfo">
<gr-file-list-header
id="fileListHeader"
account="[[_account]]"
@@ -493,7 +494,9 @@
on-expand-diffs="_expandAllDiffs"
on-collapse-diffs="_collapseAllDiffs">
</gr-file-list-header>
- <gr-file-list id="fileList"
+ <gr-file-list
+ id="fileList"
+ class="hideOnMobileOverlay"
diff-prefs="{{_diffPrefs}}"
change="[[_change]]"
change-num="[[_changeNum]]"
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 5fdef79..028d76a 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
@@ -49,6 +49,11 @@
NEW_MESSAGE: 'There are new messages on this change',
};
+ const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+ };
+
Polymer({
is: 'gr-change-view',
@@ -251,6 +256,7 @@
'shift+r': '_handleCapitalRKey',
'a': '_handleAKey',
'd': '_handleDKey',
+ 'm': '_handleMKey',
's': '_handleSKey',
'u': '_handleUKey',
'x': '_handleXKey',
@@ -311,6 +317,18 @@
});
},
+ _handleMKey(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ if (this.viewState.diffMode === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.fileListHeader.setDiffViewMode(DiffViewMode.UNIFIED);
+ } else {
+ this.$.fileListHeader.setDiffViewMode(DiffViewMode.SIDE_BY_SIDE);
+ }
+ },
+
_handleEditCommitMessage(e) {
this._editingCommitMessage = true;
this.$.commitMessageEditor.focusTextarea();
@@ -1379,8 +1397,10 @@
this.$.relatedChanges.reload();
},
- _computeHeaderClass(change) {
- return change.work_in_progress ? 'header wip' : 'header';
+ _computeHeaderClass(editMode) {
+ const classes = ['header'];
+ if (editMode) { classes.push('editMode'); }
+ return classes.join(' ');
},
_computeEditMode(patchRangeRecord, paramsRecord) {
@@ -1398,7 +1418,7 @@
case GrEditConstants.Actions.DELETE.id:
controls.openDeleteDialog(path);
break;
- case GrEditConstants.Actions.EDIT.id:
+ case GrEditConstants.Actions.OPEN.id:
Gerrit.Nav.navigateToRelativeUrl(
Gerrit.Nav.getEditUrlForDiff(this._change, path,
this._patchRange.patchNum));
@@ -1451,15 +1471,8 @@
Gerrit.Nav.navigateToChange(this._change, patchNum, null, true);
},
- /**
- * Navigate to the latest non-edit patch set.
- */
- _handleDoneEditTap() {
- let patchNum = this._patchRange.patchNum;
- if (this.patchNumEquals(patchNum, this.EDIT_NAME)) {
- patchNum = this.computeLatestPatchNum(this._allPatchSets);
- }
- Gerrit.Nav.navigateToChange(this._change, patchNum);
+ _handleStopEditTap() {
+ Gerrit.Nav.navigateToChange(this._change, this._patchRange.patchNum);
},
});
})();
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 347100e..2d2bf94 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
@@ -257,6 +257,22 @@
MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
assert.isTrue(stub.called);
});
+
+ test('m should toggle diff mode', () => {
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ const setModeStub = sandbox.stub(element.$.fileListHeader,
+ 'setDiffViewMode');
+ const e = {preventDefault: () => {}};
+ flushAsynchronousOperations();
+
+ element.viewState.diffMode = 'SIDE_BY_SIDE';
+ element._handleMKey(e);
+ assert.isTrue(setModeStub.calledWith('UNIFIED_DIFF'));
+
+ element.viewState.diffMode = 'UNIFIED_DIFF';
+ element._handleMKey(e);
+ assert.isTrue(setModeStub.calledWith('SIDE_BY_SIDE'));
+ });
});
suite('reloading drafts', () => {
@@ -1307,12 +1323,11 @@
assert.isFalse(element._computeCanStartReview(false, change, account1));
assert.isFalse(element._computeCanStartReview(true, change, account2));
});
+ });
- test('header class computation', () => {
- assert.equal(element._computeHeaderClass({}), 'header');
- assert.equal(element._computeHeaderClass({work_in_progress: true}),
- 'header wip');
- });
+ test('header class computation', () => {
+ assert.equal(element._computeHeaderClass(), 'header');
+ assert.equal(element._computeHeaderClass(true), 'header editMode');
});
test('_maybeScrollToMessage', () => {
@@ -1424,9 +1439,9 @@
assert.isTrue(controls.openRenameDialog.called);
assert.equal(controls.openRenameDialog.lastCall.args[0], 'foo');
- // Edit
+ // Open
fileList.dispatchEvent(new CustomEvent('file-action-tap',
- {detail: {action: Actions.EDIT.id, path: 'foo'}, bubbles: true}));
+ {detail: {action: Actions.OPEN.id, path: 'foo'}, bubbles: true}));
flushAsynchronousOperations();
assert.isTrue(Gerrit.Nav.getEditUrlForDiff.called);
@@ -1535,42 +1550,20 @@
});
});
- suite('_handleDoneEditTap', () => {
- let fireDone;
-
- setup(() => {
- fireDone = () => {
- element.$.actions.dispatchEvent(new CustomEvent('done-edit-tap',
- {bubbles: false}));
- };
- sandbox.stub(element.$.metadata, '_computeShowLabelStatus');
- sandbox.stub(element.$.metadata, '_computeLabelNames');
- navigateToChangeStub.restore();
+ test('_handleStopEditTap', done => {
+ sandbox.stub(element.$.metadata, '_computeShowLabelStatus');
+ sandbox.stub(element.$.metadata, '_computeLabelNames');
+ navigateToChangeStub.restore();
+ sandbox.stub(element, 'computeLatestPatchNum').returns(1);
+ sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
+ assert.equal(args.length, 2);
+ assert.equal(args[1], 1); // patchNum
+ done();
});
- test('edit patchset loaded', done => {
- sandbox.stub(element, 'computeLatestPatchNum').returns(1);
- sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
- assert.equal(args.length, 2);
- assert.equal(args[1], 1); // patchNum
- done();
- });
-
- element._patchRange = {patchNum: element.EDIT_NAME};
- fireDone();
- });
-
- test('edit patchset loaded', done => {
- sandbox.stub(element, 'computeLatestPatchNum').returns(1);
- sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => {
- assert.equal(args.length, 2);
- assert.equal(args[1], 1); // patchNum
- done();
- });
-
- element._patchRange = {patchNum: 1};
- fireDone();
- });
+ element._patchRange = {patchNum: 1};
+ element.$.actions.dispatchEvent(new CustomEvent('stop-edit-tap',
+ {bubbles: false}));
});
suite('plugin endpoints', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
new file mode 100644
index 0000000..b757c29
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-confirm-abandon-dialog</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-confirm-abandon-dialog.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-confirm-abandon-dialog></gr-confirm-abandon-dialog>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-confirm-abandon-dialog tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_handleConfirmTap', () => {
+ const confirmHandler = sandbox.stub();
+ element.addEventListener('confirm', confirmHandler);
+ sandbox.stub(element, '_confirm');
+ element.$$('gr-confirm-dialog').fire('confirm');
+ assert.isTrue(confirmHandler.called);
+ assert.isTrue(element._confirm.called);
+ });
+
+ test('_handleCancelTap', () => {
+ const cancelHandler = sandbox.stub();
+ element.addEventListener('cancel', cancelHandler);
+ sandbox.stub(element, '_handleCancelTap');
+ element.$$('gr-confirm-dialog').fire('cancel');
+ assert.isTrue(cancelHandler.called);
+ assert.isTrue(element._handleCancelTap.called);
+ });
+ });
+</script>
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 8cd0329..8aa7436 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
@@ -18,6 +18,7 @@
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+<link rel="import" href="../../diff/gr-diff-mode-selector/gr-diff-mode-selector.html">
<link rel="import" href="../../diff/gr-patch-range-select/gr-patch-range-select.html">
<link rel="import" href="../../edit/gr-edit-controls/gr-edit-controls.html">
<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html">
@@ -38,9 +39,6 @@
.collapseToggleButton {
text-decoration: none;
}
- .patchInfoEdit.patchInfo-header {
- background-color: #fcfad6;
- }
.patchInfoOldPatchSet.patchInfo-header {
background-color: #fff9c4;
}
@@ -99,9 +97,6 @@
.expanded #expandBtn {
display: none;
}
- gr-button.selected iron-icon {
- color: var(--color-link);
- }
gr-linked-chip {
--linked-chip-text-color: black;
}
@@ -140,9 +135,13 @@
font-family: var(--font-family-bold);
margin-right: 24px;
}
- gr-commit-info {
+ gr-commit-info,
+ gr-edit-controls {
margin-right: -5px;
}
+ .fileViewActionsLabel {
+ margin-right: .2rem;
+ }
@media screen and (max-width: 50em) {
.patchInfo-header .desktop {
display: none;
@@ -232,21 +231,11 @@
</template>
<div class="fileViewActions">
<span class="separator"></span>
- <span>Diff Views:</span>
- <gr-button
- id="sideBySideBtn"
- link
- has-tooltip
- title="Side-by-side diff"
- class$="[[_computeSelectedClass(diffViewMode, _VIEW_MODES.SIDE_BY_SIDE)]]"
- on-tap="_handleSideBySideTap"><iron-icon icon="gr-icons:side-by-side"></iron-icon></gr-button>
- <gr-button
- id="unifiedBtn"
- link
- has-tooltip
- title="Unified dff"
- class$="[[_computeSelectedClass(diffViewMode, _VIEW_MODES.UNIFIED)]]"
- on-tap="_handleUnifiedTap"><iron-icon icon="gr-icons:unified"></iron-icon></gr-button>
+ <span class="fileViewActionsLabel">Diff view:</span>
+ <gr-diff-mode-selector
+ id="modeSelect"
+ mode="{{diffViewMode}}"
+ save-on-change="[[loggedIn]]"></gr-diff-mode-selector>
<span id="diffPrefsContainer"
class="hideOnEdit"
hidden$="[[_computePrefsButtonHidden(diffPrefs, loggedIn)]]"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index 9d32e75..84add8a 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -56,15 +56,6 @@
type: Boolean,
computed: '_computeDescriptionReadOnly(loggedIn, change, account)',
},
- /** @type {?} */
- _VIEW_MODES: {
- type: Object,
- readOnly: true,
- value: {
- SIDE_BY_SIDE: 'SIDE_BY_SIDE',
- UNIFIED: 'UNIFIED_DIFF',
- },
- },
_revisionInfo: {
type: Object,
computed: '_getRevisionInfo(change)',
@@ -79,6 +70,10 @@
'_computePatchSetDescription(change, patchNum)',
],
+ setDiffViewMode(mode) {
+ this.$.modeSelect.setMode(mode);
+ },
+
_expandAllDiffs() {
this._expanded = true;
this.fire('expand-diffs');
@@ -89,10 +84,6 @@
this.fire('collapse-diffs');
},
- _computeSelectedClass(diffViewMode, buttonViewMode) {
- return buttonViewMode === diffViewMode ? 'selected' : '';
- },
-
_computeExpandedClass(filesExpanded) {
const classes = [];
if (filesExpanded === GrFileListConstants.FilesExpandedState.ALL) {
@@ -105,14 +96,6 @@
return classes.join(' ');
},
- _handleSideBySideTap() {
- this.diffViewMode = this._VIEW_MODES.SIDE_BY_SIDE;
- },
-
- _handleUnifiedTap() {
- this.diffViewMode = this._VIEW_MODES.UNIFIED;
- },
-
_computeDescriptionPlaceholder(readOnly) {
return (readOnly ? 'No' : 'Add') + ' patchset description';
},
@@ -206,10 +189,6 @@
},
_computePatchInfoClass(patchNum, allPatchSets) {
- if (this.patchNumEquals(patchNum, this.EDIT_NAME)) {
- return 'patchInfoEdit';
- }
-
const latestNum = this.computeLatestPatchNum(allPatchSets);
if (this.patchNumEquals(patchNum, latestNum)) {
return '';
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
index 16cbfcb..a92d783 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -194,19 +194,6 @@
});
});
- test('diff mode selector is set correctly', () => {
- const sideBySideBtn = element.$.sideBySideBtn;
- const unifiedBtn = element.$.unifiedBtn;
- element.diffViewMode = 'SIDE_BY_SIDE';
- flushAsynchronousOperations();
- assert.isTrue(sideBySideBtn.classList.contains('selected'));
- assert.isFalse(unifiedBtn.classList.contains('selected'));
- element.diffViewMode = 'UNIFIED_DIFF';
- flushAsynchronousOperations();
- assert.isFalse(sideBySideBtn.classList.contains('selected'));
- assert.isTrue(unifiedBtn.classList.contains('selected'));
- });
-
test('fileViewActions are properly hidden', () => {
const actions = element.$$('.fileViewActions');
assert.equal(getComputedStyle(actions).display, 'none');
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 bbfbe7f..68d7442 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
@@ -209,7 +209,8 @@
opacity: 100;
}
.row:hover .reviewed label,
- .row:focus .reviewed label {
+ .row:focus .reviewed label,
+ .row.expanded .reviewed label {
opacity: 100;
}
.reviewed input {
@@ -225,8 +226,13 @@
opacity: 100;
}
.editFileControls {
- margin-left: 1em;
- width: 10em;
+ width: 7em;
+ }
+ .markReviewed,
+ .pathLink {
+ display: inline-block;
+ margin: -.2em 0;
+ padding: .4em 0;
}
@media screen and (max-width: 50em) {
.desktop {
@@ -279,9 +285,9 @@
[[_computeFileStatus(file.status)]]
</div>
<span
- data-url="[[_computeDiffURL(change, patchRange.patchNum, patchRange.basePatchNum, file.__path)]]"
+ data-url="[[_computeDiffURL(change, patchRange.patchNum, patchRange.basePatchNum, file.__path, editMode)]]"
class="path">
- <a class="pathLink" href$="[[_computeDiffURL(change, patchRange.patchNum, patchRange.basePatchNum, file.__path)]]">
+ <a class="pathLink" href$="[[_computeDiffURL(change, patchRange.patchNum, patchRange.basePatchNum, file.__path, editMode)]]">
<span title$="[[computeDisplayPath(file.__path)]]"
class="fullFileName">
[[computeDisplayPath(file.__path)]]
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 b42824c..a326f54 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
@@ -137,6 +137,9 @@
value: true,
computed: '_computeShowSizeBars(_userPrefs)',
},
+
+ /** @type {Function} */
+ _cancelForEachDiff: Function,
},
behaviors: [
@@ -174,6 +177,10 @@
keydown: '_scopedKeydownHandler',
},
+ detached() {
+ this._cancelDiffs();
+ },
+
/**
* Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard
* events must be scoped to a component level (e.g. `enter`) in order to not
@@ -435,6 +442,7 @@
while (!row.classList.contains('row') && row.parentElement) {
row = row.parentElement;
}
+
const path = row.dataset.path;
// Handle checkbox mark as reviewed.
if (e.target.classList.contains('markReviewed')) {
@@ -526,7 +534,7 @@
const isRangeSelected = this.diffs.some(diff => {
return diff.isRangeSelected();
}, this);
- if (this._showInlineDiffs && !isRangeSelected) {
+ if (!isRangeSelected) {
e.preventDefault();
this._addDraftAtTarget();
}
@@ -660,7 +668,13 @@
return status || 'M';
},
- _computeDiffURL(change, patchNum, basePatchNum, path) {
+ _computeDiffURL(change, patchNum, basePatchNum, path, editMode) {
+ // TODO(kaspern): Fix editing for commit messages and merge lists.
+ if (editMode && path !== this.COMMIT_MESSAGE_PATH &&
+ path !== this.MERGE_LIST_PATH) {
+ return Gerrit.Nav.getEditUrlForDiff(change, path, patchNum,
+ basePatchNum);
+ }
return Gerrit.Nav.getUrlForDiff(change, path, patchNum, basePatchNum);
},
@@ -849,6 +863,7 @@
_clearCollapsedDiffs(collapsedDiffs) {
for (const diff of collapsedDiffs) {
+ diff.cancel();
diff.clearDiffContent();
}
},
@@ -871,7 +886,9 @@
return (new Promise(resolve => {
this.fire('reload-drafts', {resolve});
})).then(() => {
- return this.asyncForeach(paths, path => {
+ return this.asyncForeach(paths, (path, cancel) => {
+ this._cancelForEachDiff = cancel;
+
iter++;
console.log('Expanding diff', iter, 'of', initialCount, ':',
path);
@@ -884,6 +901,7 @@
}
return Promise.all(promises);
}).then(() => {
+ this._cancelForEachDiff = null;
this._nextRenderParams = null;
console.log('Finished expanding', initialCount, 'diff(s)');
this.$.reporting.timeEnd(timerName);
@@ -892,6 +910,12 @@
});
},
+ /** Cancel the rendering work of every diff in the list */
+ _cancelDiffs() {
+ if (this._cancelForEachDiff) { this._cancelForEachDiff(); }
+ this._forEachDiff(d => d.cancel());
+ },
+
/**
* In the given NodeList of diff elements, find the diff for the given path.
* @param {string} path
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 ffd91a4..38ab31b 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
@@ -494,6 +494,10 @@
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
assert.equal(element.$.fileCursor.index, 0);
assert.equal(element.selectedIndex, 0);
+
+ sandbox.stub(element, '_addDraftAtTarget');
+ MockInteractions.pressAndReleaseKeyOn(element, 67, null, 'c');
+ assert.isTrue(element._addDraftAtTarget.called);
});
test('i key shows/hides selected inline diff', () => {
@@ -851,6 +855,7 @@
reload() {
done();
},
+ cancel() {},
}];
sinon.stub(element, 'diffs', {
get() { return diffs; },
@@ -858,6 +863,16 @@
element.push('_expandedFilePaths', path);
});
+ test('_clearCollapsedDiffs', () => {
+ const diff = {
+ cancel: sinon.stub(),
+ clearDiffContent: sinon.stub(),
+ };
+ element._clearCollapsedDiffs([diff]);
+ assert.isTrue(diff.cancel.calledOnce);
+ assert.isTrue(diff.clearDiffContent.calledOnce);
+ });
+
test('filesExpanded value updates to correct enum', () => {
element._files = [{__path: 'foo.bar'}, {__path: 'baz.bar'}];
flushAsynchronousOperations();
@@ -975,6 +990,7 @@
element.change = {_number: 123};
element.patchRange = {patchNum: undefined, basePatchNum: 'PARENT'};
element._files = [{__path: 'foo/bar.cpp'}];
+ element.editMode = false;
flush(() => {
assert.isFalse(urlStub.called);
element.set('patchRange.patchNum', 4);
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 6b4499f..0e73c6e 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -16,7 +16,7 @@
const CI_LABELS = ['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'];
const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+: /;
- const LABEL_TITLE_SCORE_PATTERN = /([A-Za-z0-9-]+)([+-]\d+)/;
+ const LABEL_TITLE_SCORE_PATTERN = /^([A-Za-z0-9-]+)([+-]\d+)$/;
Polymer({
is: 'gr-message',
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index aa18c4e..658dcad 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -180,5 +180,14 @@
};
assert.isOk(Polymer.dom(element.root).querySelector('.positiveVote'));
});
+
+ test('false negative vote', () => {
+ element.message = {
+ author: {},
+ expanded: false,
+ message: 'Patch Set 1: Cherry Picked from branch stable-2.14.',
+ };
+ assert.isNotOk(Polymer.dom(element.root).querySelector('.negativeVote'));
+ });
});
</script>
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 cd737c4..ea98fb8 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
@@ -126,17 +126,17 @@
</section>
<section hidden$="[[!_submittedTogether.length]]" hidden>
<h4>Submitted together</h4>
- <template is="dom-repeat" items="[[_submittedTogether]]" as="change">
- <div>
- <a href$="[[_computeChangeURL(change._number, change.project)]]"
- class$="[[_computeLinkClass(change)]]"
- title$="[[change.project]]: [[change.branch]]: [[change.subject]]">
- [[change.project]]: [[change.branch]]: [[change.subject]]
+ <template is="dom-repeat" items="[[_submittedTogether]]" as="related">
+ <div class$="[[_computeChangeContainerClass(change, related)]]">
+ <a href$="[[_computeChangeURL(related._number, related.project)]]"
+ class$="[[_computeLinkClass(related)]]"
+ title$="[[related.project]]: [[related.branch]]: [[related.subject]]">
+ [[related.project]]: [[related.branch]]: [[related.subject]]
</a>
<span
tabindex="-1"
title="Submittable"
- class$="submittableCheck [[_computeLinkClass(change)]]">โ</span>
+ class$="submittableCheck [[_computeLinkClass(related)]]">โ</span>
</div>
</template>
</section>
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 46b803f..6deb9a0 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
@@ -171,8 +171,7 @@
</template>
<gr-overlay
id="reviewerConfirmationOverlay"
- on-iron-overlay-canceled="_cancelPendingReviewer"
- with-backdrop>
+ on-iron-overlay-canceled="_cancelPendingReviewer">
<div class="reviewerConfirmation">
Group
<span class="groupName">
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 b1e6a5a..348eabb 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
@@ -130,13 +130,13 @@
},
_computeReviewerTooltip(reviewer, change) {
- if (!change || !change.permitted_labels) return '';
+ if (!change || !change.labels) { return ''; }
const maxScores = [];
const maxPermitted = this._getMaxPermittedScores(change);
- for (const label of Object.keys(change.permitted_labels)) {
+ for (const label of Object.keys(change.labels)) {
const maxScore =
this._getReviewerPermittedScore(reviewer, change, label);
- if (isNaN(maxScore) || maxScore < 0) continue;
+ if (isNaN(maxScore) || maxScore < 0) { continue; }
if (maxScore > 0 && maxScore === maxPermitted[label]) {
maxScores.push(`${label}: +${maxScore}`);
} else {
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 985e4bb..24fc4d1 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
@@ -307,7 +307,6 @@
},
permitted_labels: {
Foo: ['-1', ' 0', '+1', '+2'],
- Bar: ['-1', ' 0', '+1', '+2'],
FooBar: ['-1', ' 0'],
},
};
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
new file mode 100644
index 0000000..2f1da97
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
@@ -0,0 +1,57 @@
+<!--
+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="../../../bower_components/paper-toggle-button/paper-toggle-button.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+<link rel="import" href="../../diff/gr-diff-comment-thread/gr-diff-comment-thread.html">
+
+
+<dom-module id="gr-thread-list">
+ <template>
+ <style include="shared-styles">
+ .draftsOnly gr-diff-comment-thread,
+ .unresolvedOnly gr-diff-comment-thread {
+ display: none
+ }
+ .draftsOnly:not(.unresolvedOnly) gr-diff-comment-thread[has-draft],
+ .unresolvedOnly:not(.draftsOnly) gr-diff-comment-thread[unresolved],
+ .draftsOnly.unresolvedOnly gr-diff-comment-thread[has-draft][unresolved] {
+ display: block
+ }
+ </style>
+ <div id="threads">
+ <div>
+ <paper-toggle-button
+ id="unresolvedToggle"
+ on-change="_toggleUnresolved"></paper-toggle-button>Only Unresolved threads
+ <paper-toggle-button
+ id="draftToggle"
+ on-change="_toggleDrafts"></paper-toggle-button>Only threads with drafts
+ </div>
+ <template is="dom-repeat" items="[[threads]]" as="thread">
+ <a href$="[[_getDiffUrlForComment(change, thread.path, thread.patchNum, thread.line)]]">[[thread.path]]</a>
+ <gr-diff-comment-thread
+ comments="[[thread.comments]]"
+ comment-side="[[thread.commentSide]]"
+ patch-num="[[thread.patchNum]]"
+ path="[[thread.path]]"></gr-diff-comment-thread>
+ </template>
+ </div>
+ </template>
+ <script src="gr-thread-list.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
new file mode 100644
index 0000000..1e4a69e
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -0,0 +1,38 @@
+// 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-thread-list',
+
+ properties: {
+ change: Object,
+ threads: Array,
+ changeNum: String,
+ },
+
+ _toggleUnresolved() {
+ this.$.threads.classList.toggle('unresolvedOnly');
+ },
+
+ _toggleDrafts() {
+ this.$.threads.classList.toggle('draftsOnly');
+ },
+
+ _getDiffUrlForComment(change, path, patchNum, lineNum) {
+ return Gerrit.Nav.getUrlForDiff(change, path, patchNum, null, lineNum);
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
new file mode 100644
index 0000000..0fef3d6d
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
@@ -0,0 +1,180 @@
+<!DOCTYPE html>
+<!--
+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-thread-list</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-thread-list.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-thread-list></gr-thread-list>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-thread-list tests', () => {
+ let element;
+ let sandbox;
+ let threadsElements;
+ const computeVisibleNumber = threads => {
+ let count = 0;
+ for (const thread of threads) {
+ if (getComputedStyle(thread).display !== 'none') {
+ count++;
+ }
+ }
+ return count;
+ };
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ element.threads = [
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 4,
+ id: 'ecf0b9fa_fe1a5f62',
+ line: 5,
+ updated: '2018-02-08 18:49:18.000000000',
+ message: 'test',
+ unresolved: true,
+ },
+ {
+ id: '503008e2_0ab203ee',
+ path: '/COMMIT_MSG',
+ line: 5,
+ in_reply_to: 'ecf0b9fa_fe1a5f62',
+ updated: '2018-02-13 22:48:48.018000000',
+ message: 'draft',
+ unresolved: true,
+ __draft: true,
+ __draftID: '0.m683trwff68',
+ __editing: false,
+ patch_set: '2',
+ },
+ ],
+ patchNum: 4,
+ path: '/COMMIT_MSG',
+ line: 5,
+ rootId: 'ecf0b9fa_fe1a5f62',
+ start_datetime: '2018-02-08 18:49:18.000000000',
+ },
+ {
+ comments: [
+ {
+ __path: 'test.txt',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 3,
+ id: '09a9fb0a_1484e6cf',
+ side: 'PARENT',
+ updated: '2018-02-13 22:47:19.000000000',
+ message: 'Some comment on another patchset.',
+ unresolved: false,
+ },
+ ],
+ patchNum: 3,
+ path: 'test.txt',
+ rootId: '09a9fb0a_1484e6cf',
+ start_datetime: '2018-02-13 22:47:19.000000000',
+ commentSide: 'PARENT',
+ },
+ {
+ comments: [
+ {
+ __path: '/COMMIT_MSG',
+ author: {
+ _account_id: 1000000,
+ name: 'user',
+ username: 'user',
+ },
+ patch_set: 2,
+ id: '8caddf38_44770ec1',
+ line: 4,
+ updated: '2018-02-13 22:48:40.000000000',
+ message: 'Another unresolved comment',
+ unresolved: true,
+ },
+ ],
+ patchNum: 2,
+ path: '/COMMIT_MSG',
+ line: 4,
+ rootId: '8caddf38_44770ec1',
+ start_datetime: '2018-02-13 22:48:40.000000000',
+ },
+ ];
+ flushAsynchronousOperations();
+ threadsElements = Polymer.dom(element.root)
+ .querySelectorAll('gr-diff-comment-thread');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('there are three threads by default', () => {
+ assert.equal(computeVisibleNumber(threadsElements), 3);
+ });
+
+ test('toggle unresolved only shows unressolved comments', () => {
+ MockInteractions.tap(element.$.unresolvedToggle);
+ flushAsynchronousOperations();
+ assert.equal(computeVisibleNumber(threadsElements), 2);
+ });
+
+ test('toggle drafts only shows threads with draft comments', () => {
+ MockInteractions.tap(element.$.draftToggle);
+ flushAsynchronousOperations();
+ assert.equal(computeVisibleNumber(threadsElements), 1);
+ });
+
+ test('toggle drafts and unresolved only shows threads with drafts and ' +
+ 'unresolved', () => {
+ MockInteractions.tap(element.$.draftToggle);
+ MockInteractions.tap(element.$.unresolvedToggle);
+ flushAsynchronousOperations();
+ assert.equal(computeVisibleNumber(threadsElements), 1);
+ });
+
+ test('_getDiffUrlForComment', () => {
+ sandbox.stub(Gerrit.Nav, 'getUrlForDiff');
+ const change = {_number: 123, project: 'demo-project'};
+ const path = '/path';
+ const patchNum = 2;
+ const lineNum = 5;
+ element._getDiffUrlForComment(change, path, patchNum, lineNum);
+ assert.isTrue(Gerrit.Nav.getUrlForDiff.lastCall.calledWithExactly(
+ change, path, patchNum, null, lineNum));
+ });
+ });
+</script>
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 c30f726..0d8bbbb 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
@@ -379,6 +379,12 @@
</tr>
<tr>
<td>
+ <span class="key">m</span>
+ </td>
+ <td>Toggle unified/side-by-side diff</td>
+ </tr>
+ <tr>
+ <td>
<span class="key">c</span>
</td>
<td>Draft new comment</td>
@@ -453,6 +459,12 @@
</tr>
<tr>
<td>
+ <span class="key">m</span>
+ </td>
+ <td>Toggle unified/side-by-side diff</td>
+ </tr>
+ <tr>
+ <td>
<span class="key">c</span>
</td>
<td>Draft new comment</td>
@@ -486,11 +498,9 @@
<td><span class="key">,</span></td>
<td>Show diff preferences</td>
</tr>
- <tr>
- <td>
- <span class="key">v</span>
- </td>
- <td>Toggle Unified/Side-by-side diff</td>
+ <tr>
+ <td><span class="key">r</span></td>
+ <td>Mark/unmark file as reviewed</td>
</tr>
</tbody>
</table>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index fcb4ecc..4647e52 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -45,6 +45,7 @@
// - `statuses`, optional, Array<String>: the list of change statuses to
// search for. If more than one is provided, the search will OR them
// together.
+ // - `offset`, optional, Number: the offset for the query.
//
// - Gerrit.Nav.View.DIFF:
// - `changeNum`, required, String: the numeric ID of the change.
@@ -163,10 +164,11 @@
return this._generateUrl(params);
},
- getUrlForSearchQuery(query) {
+ getUrlForSearchQuery(query, opt_offset) {
return this._getUrlFor({
view: Gerrit.Nav.View.SEARCH,
query,
+ offset: opt_offset,
});
},
@@ -291,11 +293,12 @@
* @param {number=} opt_patchNum
* @param {number|string=} opt_basePatchNum The string 'PARENT' can be
* used for none.
+ * @param {number|string=} opt_lineNum
* @return {string}
*/
- getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum) {
+ getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum, opt_lineNum) {
return this.getUrlForDiffById(change._number, change.project, path,
- opt_patchNum, opt_basePatchNum);
+ opt_patchNum, opt_basePatchNum, opt_lineNum);
},
/**
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 0d38ce0..322d406 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -334,8 +334,13 @@
* @return {string}
*/
_generateSearchUrl(params) {
+ let offsetExpr = '';
+ if (params.offset && params.offset > 0) {
+ offsetExpr = ',' + params.offset;
+ }
+
if (params.query) {
- return '/q/' + this.encodeURL(params.query, true);
+ return '/q/' + this.encodeURL(params.query, true) + offsetExpr;
}
const operators = [];
@@ -367,7 +372,8 @@
')');
}
}
- return '/q/' + operators.join('+');
+
+ return '/q/' + operators.join('+') + offsetExpr;
},
/**
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 f0ec58d..d0beb91 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
@@ -230,10 +230,19 @@
'/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
'topic:"g%2525h"+status:op%2525en');
+ params.offset = 100;
+ assert.equal(element._generateUrl(params),
+ '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' +
+ 'topic:"g%2525h"+status:op%2525en,100');
+ delete params.offset;
+
// The presence of the query param overrides other params.
params.query = 'foo$bar';
assert.equal(element._generateUrl(params), '/q/foo%2524bar');
+ params.offset = 100;
+ assert.equal(element._generateUrl(params), '/q/foo%2524bar,100');
+
params = {
view: Gerrit.Nav.View.SEARCH,
statuses: ['a', 'b', 'c'],
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 9f7c5c0..522ee52 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -349,20 +349,33 @@
return result;
};
+ /**
+ * @param {number} changeNum
+ * @param {number|string} patchNum
+ * @param {string} path
+ * @param {boolean} isOnParent
+ * @param {string} commentSide
+ * @return {!Object}
+ */
GrDiffBuilder.prototype.createCommentThreadGroup = function(changeNum,
- patchNum, path, isOnParent, range) {
+ patchNum, path, isOnParent, commentSide) {
const threadGroupEl =
document.createElement('gr-diff-comment-thread-group');
threadGroupEl.changeNum = changeNum;
+ threadGroupEl.commentSide = commentSide;
threadGroupEl.patchForNewThreads = patchNum;
threadGroupEl.path = path;
threadGroupEl.isOnParent = isOnParent;
threadGroupEl.projectName = this._projectName;
- threadGroupEl.range = range;
threadGroupEl.parentIndex = this._parentIndex;
return threadGroupEl;
};
+ /**
+ * @param {number} line
+ * @param {string=} opt_side
+ * @return {!Object}
+ */
GrDiffBuilder.prototype._commentThreadGroupForLine = function(
line, opt_side) {
const comments =
@@ -385,7 +398,7 @@
}
const threadGroupEl = this.createCommentThreadGroup(
this._comments.meta.changeNum, patchNum, this._comments.meta.path,
- isOnParent);
+ isOnParent, opt_side);
threadGroupEl.comments = comments;
if (opt_side) {
threadGroupEl.setAttribute('data-side', opt_side);
@@ -411,9 +424,11 @@
} else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) {
td.classList.add('contextLineNum');
td.setAttribute('data-value', '@@');
+ td.textContent = '@@';
} else if (line.type === GrDiffLine.Type.BOTH || line.type === type) {
td.classList.add('lineNum');
td.setAttribute('data-value', number);
+ td.textContent = number === 'FILE' ? 'File' : number;
}
return td;
};
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 bf94bb6..407cf93 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
@@ -32,14 +32,15 @@
<template is="dom-repeat" items="[[_threads]]" as="thread">
<gr-diff-comment-thread
comments="[[thread.comments]]"
- comment-side="[[thread.commentSide]]"
+ comment-side="[[commentSide]]"
is-on-parent="[[isOnParent]]"
parent-index="[[parentIndex]]"
change-num="[[changeNum]]"
- location-range="[[thread.locationRange]]"
patch-num="[[thread.patchNum]]"
+ root-id="[[thread.rootId]]"
path="[[path]]"
- project-name="[[projectName]]"></gr-diff-comment-thread>
+ project-name="[[projectName]]"
+ range="[[thread.range]]"></gr-diff-comment-thread>
</template>
</template>
<script src="gr-diff-comment-thread-group.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js
index 6978d05..1654235 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js
@@ -19,6 +19,7 @@
properties: {
changeNum: String,
+ commentSide: String,
comments: {
type: Array,
value() { return []; },
@@ -44,36 +45,66 @@
'_commentsChanged(comments.*)',
],
- addNewThread(locationRange) {
+ /**
+ * Adds a new thread. Range is optional because a comment can be
+ * added to a line without a range selected.
+ *
+ * @param {!Object} opt_range
+ */
+ addNewThread(opt_range) {
this.push('_threads', {
comments: [],
- locationRange,
patchNum: this.patchForNewThreads,
+ range: opt_range,
});
},
- removeThread(locationRange) {
+ removeThread(rootId) {
for (let i = 0; i < this._threads.length; i++) {
- if (this._threads[i].locationRange === locationRange) {
+ if (this._threads[i].rootId === rootId) {
this.splice('_threads', i, 1);
return;
}
}
},
- getThreadForRange(rangeToCheck) {
- const threads = [].filter.call(
- Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread'),
- thread => {
- return thread.locationRange === rangeToCheck;
- });
+ /**
+ * Fetch the thread group at the given range, or the range-less thread
+ * on the line if no range is provided.
+ *
+ * @param {!Object=} opt_range
+ * @return {!Object|undefined}
+ */
+ getThread(opt_range) {
+ const threadEls =
+ Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
+ const threads = [].filter.call(threadEls,
+ thread => this._rangesEqual(thread.range, opt_range));
if (threads.length === 1) {
return threads[0];
}
},
+ /**
+ * Compare two ranges. Either argument may be falsy, but will only return
+ * true if both are falsy or if neither are falsy and have the same position
+ * values.
+ *
+ * @param {Object=} a range 1
+ * @param {Object=} b range 2
+ * @return {boolean}
+ */
+ _rangesEqual(a, b) {
+ if (!a && !b) { return true; }
+ if (!a || !b) { return false; }
+ return a.startLine === b.startLine &&
+ a.startChar === b.startChar &&
+ a.endLine === b.endLine &&
+ a.endChar === b.endChar;
+ },
+
_commentsChanged() {
- this._threads = this._getThreadGroups(this.comments);
+ this._threads = this._getThreads(this.comments);
},
_sortByDate(threadGroups) {
@@ -110,19 +141,12 @@
return comment.patchNum || this.patchForNewThreads;
},
- _getThreadGroups(comments) {
+ _getThreads(comments) {
const sortedComments = comments.slice(0).sort((a, b) =>
util.parseDate(a.updated) - util.parseDate(b.updated));
const threads = [];
for (const comment of sortedComments) {
- let locationRange;
- if (!comment.range) {
- locationRange = 'line-' + comment.__commentSide;
- } else {
- locationRange = this._calculateLocationRange(comment.range, comment);
- }
-
// If the comment is in reply to another comment, find that comment's
// thread and append to it.
if (comment.in_reply_to) {
@@ -135,13 +159,17 @@
}
// Otherwise, this comment starts its own thread.
- threads.push({
+ const newThread = {
start_datetime: comment.updated,
comments: [comment],
- locationRange,
commentSide: comment.__commentSide,
patchNum: this._getPatchNum(comment),
- });
+ rootId: comment.id,
+ };
+ if (comment.range) {
+ newThread.range = Object.assign({}, comment.range);
+ }
+ threads.push(newThread);
}
return threads;
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
index 713a162..0abd0b8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
@@ -50,7 +50,7 @@
sandbox.restore();
});
- test('_getThreadGroups', () => {
+ test('_getThreads', () => {
element.patchForNewThreads = 3;
const comments = [
{
@@ -83,13 +83,12 @@
__commentSide: 'left',
in_reply_to: 'sallys_confession',
}],
- locationRange: 'line-left',
+ rootId: 'sallys_confession',
patchNum: 3,
},
];
- assert.deepEqual(element._getThreadGroups(comments),
- expectedThreadGroups);
+ assert.deepEqual(element._getThreads(comments), expectedThreadGroups);
// Patch num should get inherited from comment rather
comments.push({
@@ -122,7 +121,7 @@
__commentSide: 'left',
}],
patchNum: 3,
- locationRange: 'line-left',
+ rootId: 'sallys_confession',
},
{
start_datetime: '2015-12-24 15:00:10.396000000',
@@ -140,12 +139,17 @@
__commentSide: 'left',
}],
patchNum: 3,
- locationRange: 'range-1-1-1-2-left',
+ rootId: 'betsys_confession',
+ range: {
+ start_line: 1,
+ start_character: 1,
+ end_line: 1,
+ end_character: 2,
+ },
},
];
- assert.deepEqual(element._getThreadGroups(comments),
- expectedThreadGroups);
+ assert.deepEqual(element._getThreads(comments), expectedThreadGroups);
});
test('multiple comments at same location but not threaded', () => {
@@ -163,7 +167,7 @@
__commentSide: 'left',
},
];
- assert.equal(element._getThreadGroups(comments).length, 2);
+ assert.equal(element._getThreads(comments).length, 2);
});
test('_sortByDate', () => {
@@ -277,5 +281,23 @@
flushAsynchronousOperations();
assert(element._threads.length, 1);
});
+
+ test('_rangesEqual', () => {
+ const range1 =
+ {startLine: 123, startChar: 345, endLine: 234, endChar: 456};
+ const range2 =
+ {startLine: 1, startChar: 2, endLine: 3, endChar: 4};
+
+ assert.isTrue(element._rangesEqual(null, null));
+ assert.isTrue(element._rangesEqual(null, undefined));
+ assert.isTrue(element._rangesEqual(undefined, null));
+ assert.isTrue(element._rangesEqual(undefined, undefined));
+
+ assert.isFalse(element._rangesEqual(range1, null));
+ assert.isFalse(element._rangesEqual(null, range1));
+ assert.isFalse(element._rangesEqual(range1, range2));
+
+ assert.isTrue(element._rangesEqual(range1, Object.assign({}, range1)));
+ });
});
</script>
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 6a12118..f300992 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
@@ -65,6 +65,7 @@
show-actions="[[_showActions]]"
comment-side="[[comment.__commentSide]]"
side="[[comment.side]]"
+ root-id="[[rootId]]"
project-config="[[_projectConfig]]"
on-create-fix-comment="_handleCommentFix"
on-comment-discard="_handleCommentDiscard"></gr-diff-comment>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
index d019c62..4de073e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -32,7 +32,7 @@
type: Array,
value() { return []; },
},
- locationRange: String,
+ range: Object,
keyEventTarget: {
type: Object,
value() { return document.body; },
@@ -57,6 +57,7 @@
type: Number,
value: null,
},
+ rootId: String,
unresolved: {
type: Boolean,
notify: true,
@@ -129,6 +130,7 @@
if (this._orderedComments.length) {
this._lastComment = this._getLastComment();
this.unresolved = this._lastComment.unresolved;
+ this.hasDraft = this._lastComment.__draft;
}
},
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 bc8ddd0..514bc80 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
@@ -309,15 +309,17 @@
</div>
</div>
<div class="actions robotActions" hidden$="[[!_showRobotActions]]">
- <gr-endpoint-decorator name="robot-comment-controls">
- <gr-endpoint-param name="comment" value="[[comment]]">
- </gr-endpoint-param>
- </gr-endpoint-decorator>
- <gr-button link class="action fix"
- on-tap="_handleFix"
- disabled="[[robotButtonDisabled]]">
- Please Fix
- </gr-button>
+ <template is="dom-if" if="[[isRobotComment]]">
+ <gr-endpoint-decorator name="robot-comment-controls">
+ <gr-endpoint-param name="comment" value="[[comment]]">
+ </gr-endpoint-param>
+ </gr-endpoint-decorator>
+ <gr-button link class="action fix"
+ on-tap="_handleFix"
+ disabled="[[robotButtonDisabled]]">
+ Please Fix
+ </gr-button>
+ </template>
</div>
</div>
</div>
@@ -331,6 +333,7 @@
<gr-confirm-dialog
id="confirmDiscardDialog"
confirm-label="Discard"
+ confirm-on-enter
on-confirm="_handleConfirmDiscard"
on-cancel="_closeConfirmDiscardOverlay">
<div class="header" slot="header">
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
new file mode 100644
index 0000000..4c4e314
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.html
@@ -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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<dom-module id="gr-diff-mode-selector">
+ <template>
+ <style include="shared-styles">
+ :host {
+ /* Used to remove horizontal whitespace between the icons. */
+ display: flex;
+ }
+ gr-button.selected iron-icon {
+ color: var(--color-link);
+ }
+ iron-icon {
+ height: 1.3rem;
+ width: 1.3rem;
+ }
+ </style>
+ <gr-button
+ id="sideBySideBtn"
+ link
+ has-tooltip
+ class$="[[_computeSelectedClass(mode, _VIEW_MODES.SIDE_BY_SIDE)]]"
+ title="Side-by-side diff"
+ on-tap="_handleSideBySideTap">
+ <iron-icon icon="gr-icons:side-by-side"></iron-icon>
+ </gr-button>
+ <gr-button
+ id="unifiedBtn"
+ link
+ has-tooltip
+ title="Unified diff"
+ class$="[[_computeSelectedClass(mode, _VIEW_MODES.UNIFIED)]]"
+ on-tap="_handleUnifiedTap">
+ <iron-icon icon="gr-icons:unified"></iron-icon>
+ </gr-button>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </template>
+ <script src="gr-diff-mode-selector.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
new file mode 100644
index 0000000..6c3a713
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
@@ -0,0 +1,68 @@
+// 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-diff-mode-selector',
+
+ properties: {
+ mode: {
+ type: String,
+ notify: true,
+ },
+
+ /**
+ * If set to true, the user's preference will be updated every time a
+ * button is tapped. Don't set to true if there is no user.
+ */
+ saveOnChange: {
+ type: Boolean,
+ value: false,
+ },
+
+ /** @type {?} */
+ _VIEW_MODES: {
+ type: Object,
+ readOnly: true,
+ value: {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+ },
+ },
+ },
+
+ /**
+ * Set the mode. If save on change is enabled also update the preference.
+ */
+ setMode(newMode) {
+ if (this.saveOnChange && this.mode && this.mode !== newMode) {
+ this.$.restAPI.savePreferences({diff_view: newMode});
+ }
+ this.mode = newMode;
+ },
+
+ _computeSelectedClass(diffViewMode, buttonViewMode) {
+ return buttonViewMode === diffViewMode ? 'selected' : '';
+ },
+
+ _handleSideBySideTap() {
+ this.setMode(this._VIEW_MODES.SIDE_BY_SIDE);
+ },
+
+ _handleUnifiedTap() {
+ this.setMode(this._VIEW_MODES.UNIFIED);
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
new file mode 100644
index 0000000..c791091
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<!--
+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-diff-mode-selector</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"/>
+<script src="../../../bower_components/page/page.js"></script>
+<script src="../../../scripts/util.js"></script>
+
+<link rel="import" href="gr-diff-mode-selector.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-diff-mode-selector></gr-diff-mode-selector>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff-mode-selector tests', () => {
+ let element;
+ let sandbox;
+
+ setup(() => {
+ sandbox = sinon.sandbox.create();
+ element = fixture('basic');
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ });
+
+ test('_computeSelectedClass', () => {
+ assert.equal(
+ element._computeSelectedClass('SIDE_BY_SIDE', 'SIDE_BY_SIDE'),
+ 'selected');
+ assert.equal(
+ element._computeSelectedClass('SIDE_BY_SIDE', 'UNIFIED_DIFF'), '');
+ });
+
+ test('setMode', () => {
+ const saveStub = sandbox.stub(element.$.restAPI, 'savePreferences');
+
+ // Setting the mode initially does not save prefs.
+ element.saveOnChange = true;
+ element.setMode('SIDE_BY_SIDE');
+ assert.isFalse(saveStub.called);
+
+ // Setting the mode to itself does not save prefs.
+ element.setMode('SIDE_BY_SIDE');
+ assert.isFalse(saveStub.called);
+
+ // Setting the mode to something else does not save prefs if saveOnChange
+ // is false.
+ element.saveOnChange = false;
+ element.setMode('UNIFIED_DIFF');
+ assert.isFalse(saveStub.called);
+
+ // Setting the mode to something else does not save prefs if saveOnChange
+ // is false.
+ element.saveOnChange = true;
+ element.setMode('SIDE_BY_SIDE');
+ assert.isTrue(saveStub.calledOnce);
+ });
+ });
+</script>
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 ab2077d..9b830cf 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
@@ -76,7 +76,6 @@
margin-left: 1em;
}
</style>
-
<gr-overlay id="prefsOverlay" with-backdrop>
<div class="header">
Diff View Preferences
@@ -141,6 +140,14 @@
<input is="iron-input" type="checkbox" id="syntaxHighlightInput"
on-tap="_handleSyntaxHighlightTap">
</div>
+ <div class="pref">
+ <label for="automaticReviewInput">Automatically mark viewed files reviewed</label>
+ <input
+ is="iron-input"
+ id="automaticReviewInput"
+ type="checkbox"
+ on-tap="_handleAutomaticReviewTap">
+ </div>
</div>
<div class="actions">
<gr-button id="cancelButton" link on-tap="_handleCancel">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
index 185b047..ccc5361 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
@@ -63,6 +63,7 @@
this.$.showTrailingWhitespaceInput.checked = prefs.show_whitespace_errors;
this.$.lineWrappingInput.checked = prefs.line_wrapping;
this.$.syntaxHighlightInput.checked = prefs.syntax_highlighting;
+ this.$.automaticReviewInput.checked = !prefs.manual_review;
},
_localPrefsChanged(changeRecord) {
@@ -93,6 +94,10 @@
this.set('_newPrefs.line_wrapping', Polymer.dom(e).rootTarget.checked);
},
+ _handleAutomaticReviewTap(e) {
+ this.set('_newPrefs.manual_review', !Polymer.dom(e).rootTarget.checked);
+ },
+
_handleSave(e) {
e.stopPropagation();
this.prefs = this._newPrefs;
@@ -115,11 +120,6 @@
this.$.prefsOverlay.close();
},
- _handlePrefsTap(e) {
- e.preventDefault();
- this._openPrefs();
- },
-
open() {
this.$.prefsOverlay.open().then(() => {
const focusStops = this.getFocusStops();
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 107ae2f..36ae32a 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
@@ -31,6 +31,7 @@
<link rel="import" href="../../shared/revision-info/revision-info.html">
<link rel="import" href="../gr-comment-api/gr-comment-api.html">
<link rel="import" href="../gr-diff-cursor/gr-diff-cursor.html">
+<link rel="import" href="../gr-diff-mode-selector/gr-diff-mode-selector.html">
<link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html">
<link rel="import" href="../gr-diff/gr-diff.html">
<link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html">
@@ -132,6 +133,17 @@
align-items: center;
display: flex;
}
+ .diffModeSelector {
+ align-items: center;
+ display: flex;
+ }
+ .diffModeSelector span {
+ margin-right: .2rem;
+ }
+ .diffModeSelector.hide,
+ .separator.hide {
+ display: none;
+ }
gr-dropdown-list {
--trigger-style: {
text-transform: none;
@@ -259,19 +271,17 @@
</span>
</div>
<div class="rightControls">
- <gr-select
- id="modeSelect"
- bind-value="{{changeViewState.diffMode}}"
- hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">
- <select>
- <option value="SIDE_BY_SIDE">Side By Side</option>
- <option value="UNIFIED_DIFF">Unified</option>
- </select>
- </gr-select>
+ <div class$="diffModeSelector [[_computeModeSelectHideClass(_isImageDiff)]]">
+ <span>Diff view:</span>
+ <gr-diff-mode-selector
+ id="modeSelect"
+ save-on-change="[[_loggedIn]]"
+ mode="{{changeViewState.diffMode}}"></gr-diff-mode-selector>
+ </div>
<span id="diffPrefsContainer"
hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]" hidden>
<span class="preferences desktop">
- <span class="separator" hidden$="[[_computeModeSelectHidden(_isImageDiff)]]"></span>
+ <span class$="separator [[_computeModeSelectHideClass(_isImageDiff)]]"></span>
<gr-button link
class="prefsButton"
on-tap="_handlePrefsTap">Preferences</gr-button>
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 b4dd50a..cd06174 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
@@ -25,6 +25,11 @@
RIGHT: 'right',
};
+ const DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+ };
+
Polymer({
is: 'gr-diff-view',
@@ -169,7 +174,7 @@
observers: [
'_getProjectConfig(_change.project)',
'_getFiles(_changeNum, _patchRange.*)',
- '_setReviewedObserver(_loggedIn, params.*)',
+ '_setReviewedObserver(_loggedIn, params.*, _prefs)',
],
keyBindings: {
@@ -186,7 +191,8 @@
'a shift+a': '_handleAKey',
'u': '_handleUKey',
',': '_handleCommaKey',
- 'v': '_handleVKey',
+ 'm': '_handleMKey',
+ 'r': '_handleRKey',
},
attached() {
@@ -261,6 +267,14 @@
this._patchRange.patchNum, this._path, reviewed);
},
+ _handleRKey(e) {
+ if (this.shouldSuppressKeyboardShortcut(e) ||
+ this.modifierPressed(e)) { return; }
+
+ e.preventDefault();
+ this._setReviewed(!this.$.reviewed.checked);
+ },
+
_handleEscKey(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
@@ -426,15 +440,15 @@
this.$.diffPreferences.open();
},
- _handleVKey(e) {
+ _handleMKey(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
this.modifierPressed(e)) { return; }
e.preventDefault();
- if (this.changeViewState.diffMode=='SIDE_BY_SIDE') {
- this.set('changeViewState.diffMode', 'UNIFIED_DIFF');
+ if (this._getDiffViewMode() === DiffViewMode.SIDE_BY_SIDE) {
+ this.$.modeSelect.setMode(DiffViewMode.UNIFIED);
} else {
- this.set('changeViewState.diffMode', 'SIDE_BY_SIDE');
+ this.$.modeSelect.setMode(DiffViewMode.SIDE_BY_SIDE);
}
},
@@ -615,9 +629,11 @@
}
},
- _setReviewedObserver(_loggedIn, paramsRecord) {
+ _setReviewedObserver(_loggedIn, paramsRecord, _prefs) {
const params = paramsRecord.base || {};
- if (_loggedIn && params.view === Gerrit.Nav.View.DIFF) {
+ if (!_loggedIn || _prefs.manual_review) { return; }
+
+ if (params.view === Gerrit.Nav.View.DIFF) {
this._setReviewed(true);
}
},
@@ -807,15 +823,15 @@
if (this.changeViewState.diffMode) {
return this.changeViewState.diffMode;
} else if (this._userPrefs) {
- return this.changeViewState.diffMode =
- this._userPrefs.default_diff_view;
+ this.set('changeViewState.diffMode', this._userPrefs.default_diff_view);
+ return this._userPrefs.default_diff_view;
} else {
return 'SIDE_BY_SIDE';
}
},
- _computeModeSelectHidden() {
- return this._isImageDiff;
+ _computeModeSelectHideClass(isImageDiff) {
+ return isImageDiff ? 'hide' : '';
},
_onLineSelected(e, detail) {
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 6cb92e7..6dff570 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
@@ -158,6 +158,15 @@
MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
assert(computeContainerClassStub.lastCall.calledWithExactly(
false, 'SIDE_BY_SIDE', false));
+
+ sandbox.stub(element, '_setReviewed');
+ element.$.reviewed.checked = false;
+ MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r');
+ assert.isFalse(element._setReviewed.called);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r');
+ assert.isTrue(element._setReviewed.called);
+ assert.equal(element._setReviewed.lastCall.args[0], true);
});
test('keyboard shortcuts with patch range', () => {
@@ -509,6 +518,29 @@
assert.isTrue(link.hasAttribute('download'));
});
+ test('_prefs.manual_review is respected', () => {
+ const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
+ () => Promise.resolve());
+ sandbox.stub(element.$.diff, 'reload');
+
+ element._loggedIn = true;
+ element.params = {
+ view: Gerrit.Nav.View.DIFF,
+ changeNum: '42',
+ patchNum: '2',
+ basePatchNum: '1',
+ path: '/COMMIT_MSG',
+ };
+ element._prefs = {manual_review: true};
+ flushAsynchronousOperations();
+
+ assert.isFalse(saveReviewedStub.called);
+ element._prefs = {};
+ flushAsynchronousOperations();
+
+ assert.isTrue(saveReviewedStub.called);
+ });
+
test('file review status', () => {
const saveReviewedStub = sandbox.stub(element, '_saveReviewedState',
() => Promise.resolve());
@@ -522,6 +554,7 @@
basePatchNum: '1',
path: '/COMMIT_MSG',
};
+ element._prefs = {};
flushAsynchronousOperations();
const commitMsg = Polymer.dom(element.root).querySelector(
@@ -582,21 +615,21 @@
element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
// The mode selected in the view state reflects the selected option.
- assert.equal(element._getDiffViewMode(), select.nativeSelect.value);
+ assert.equal(element._getDiffViewMode(), select.mode);
// The mode selected in the view state reflects the view rednered in the
// diff.
- assert.equal(select.nativeSelect.value, diffDisplay.viewMode);
+ assert.equal(select.mode, diffDisplay.viewMode);
// We will simulate a user change of the selected mode.
const newMode = 'UNIFIED_DIFF';
- // Set the actual value of the select, and simulate the change event.
- select.nativeSelect.value = newMode;
- element.fire('change', {}, {node: select.nativeSelect});
+
+ // Set the mode, and simulate the change event.
+ element.set('changeViewState.diffMode', newMode);
// Make sure the handler was called and the state is still coherent.
assert.equal(element._getDiffViewMode(), newMode);
- assert.equal(element._getDiffViewMode(), select.nativeSelect.value);
+ assert.equal(element._getDiffViewMode(), select.mode);
assert.equal(element._getDiffViewMode(), diffDisplay.viewMode);
});
@@ -609,17 +642,16 @@
// Attach a new gr-diff-view so we can intercept the preferences fetch.
const view = document.createElement('gr-diff-view');
- const select = view.$.modeSelect;
fixture('blank').appendChild(view);
flushAsynchronousOperations();
// At this point the diff mode doesn't yet have the user's preference.
- assert.equal(select.nativeSelect.value, 'SIDE_BY_SIDE');
+ assert.equal(view._getDiffViewMode(), 'SIDE_BY_SIDE');
// Receive the overriding preference.
resolvePrefs({default_diff_view: 'UNIFIED'});
flushAsynchronousOperations();
- assert.equal(select.nativeSelect.value, 'SIDE_BY_SIDE');
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
});
suite('_commitRange', () => {
@@ -774,6 +806,19 @@
assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
});
+ test('_handleMKey', () => {
+ sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false);
+ const e = {preventDefault: () => {}};
+ // Initial state.
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+
+ element._handleMKey(e);
+ assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
+
+ element._handleMKey(e);
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ });
+
suite('_loadComments', () => {
test('empty', done => {
element._loadComments().then(() => {
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 59acafe..a019664 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -17,6 +17,7 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
<link rel="import" href="../../../styles/shared-styles.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-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../gr-diff-builder/gr-diff-builder.html">
@@ -81,10 +82,6 @@
.diff-row.target-row.target-side-right .lineNum.right,
.diff-row.target-row.unified .lineNum {
background-color: #BBDEFB;
- }
- .diff-row.target-row.target-side-left .lineNum.left:before,
- .diff-row.target-row.target-side-right .lineNum.right:before,
- .diff-row.target-row.unified .lineNum:before {
color: #000;
}
.blank,
@@ -105,22 +102,20 @@
vertical-align: top;
white-space: pre;
}
- .contextLineNum:before,
- .lineNum:before {
- box-sizing: border-box;
- display: inline-block;
+ .contextLineNum,
+ .lineNum {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+
color: #666;
- content: attr(data-value);
padding: 0 .5em;
text-align: right;
- width: 100%;
}
- .canComment .lineNum[data-value] {
+ .canComment .lineNum {
cursor: pointer;
}
- .canComment .lineNum[data-value="FILE"]:before {
- content: 'File';
- }
.content {
overflow: hidden;
/* Set max and min width since setting width on table cells still
@@ -295,6 +290,7 @@
</gr-button>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ <gr-reporting id="reporting" category="diff"></gr-reporting>
</template>
<script src="gr-diff-line.js"></script>
<script src="gr-diff-group.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 4ad35fc..5c870f2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -15,6 +15,8 @@
'use strict';
const ERR_COMMENT_ON_EDIT = 'You cannot comment on an edit.';
+ const ERR_COMMENT_ON_EDIT_BASE = 'You cannot comment on the base patch set ' +
+ 'of an edit.';
const ERR_INVALID_LINE = 'Invalid line number: ';
const MSG_EMPTY_BLAME = 'No blame information for this diff.';
@@ -182,7 +184,7 @@
/** @return {!Promise} */
reload() {
- this.$.diffBuilder.cancel();
+ this.cancel();
this.clearBlame();
this._safetyBypass = null;
this._showWarning = false;
@@ -203,6 +205,11 @@
});
},
+ /** Cancel any remaining diff builder rendering work. */
+ cancel() {
+ this.$.diffBuilder.cancel();
+ },
+
/** @return {!Array<!HTMLElement>} */
getCursorStops() {
if (this.hidden && this.noAutoRender) {
@@ -353,9 +360,16 @@
this.patchRange.basePatchNum :
this.patchRange.patchNum;
- if (this.patchNumEquals(patchNum, this.EDIT_NAME)) {
+ const isEdit = this.patchNumEquals(patchNum, this.EDIT_NAME);
+ const isEditBase = this.patchNumEquals(patchNum, this.PARENT_NAME) &&
+ this.patchNumEquals(this.patchRange.patchNum, this.EDIT_NAME);
+
+ if (isEdit) {
this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT});
return false;
+ } else if (isEditBase) {
+ this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT_BASE});
+ return false;
}
return true;
});
@@ -375,13 +389,21 @@
const patchNum = this._getPatchNumByLineAndContent(lineEl, contentEl);
const isOnParent =
this._getIsParentCommentByLineAndContent(lineEl, contentEl);
- const threadEl = this._getOrCreateThreadAtLineRange(contentEl, patchNum,
+ const threadEl = this._getOrCreateThread(contentEl, patchNum,
side, isOnParent, opt_range);
threadEl.addOrEditDraft(opt_lineNum, opt_range);
},
- _getThreadForRange(threadGroupEl, rangeToCheck) {
- return threadGroupEl.getThreadForRange(rangeToCheck);
+ /**
+ * Fetch the thread group at the given range, or the range-less thread
+ * on the line if no range is provided.
+ *
+ * @param {!Object} threadGroupEl
+ * @param {!Object=} opt_range
+ * @return {!Object}
+ */
+ _getThread(threadGroupEl, opt_range) {
+ return threadGroupEl.getThread(opt_range);
},
_getThreadGroupForLine(contentEl) {
@@ -403,31 +425,32 @@
},
/**
+ * Gets or creates a comment thread for a specific spot on a diff.
+ * May include a range, if the comment is a range comment.
+ *
* @param {!Object} contentEl
* @param {number} patchNum
* @param {string} commentSide
* @param {boolean} isOnParent
* @param {!Object=} opt_range
+ * @return {!Object}
*/
- _getOrCreateThreadAtLineRange(contentEl, patchNum, commentSide,
+ _getOrCreateThread(contentEl, patchNum, commentSide,
isOnParent, opt_range) {
- const rangeToCheck = this._getRangeString(commentSide, opt_range);
-
// Check if thread group exists.
let threadGroupEl = this._getThreadGroupForLine(contentEl);
if (!threadGroupEl) {
threadGroupEl = this.$.diffBuilder.createCommentThreadGroup(
- this.changeNum, patchNum, this.path, isOnParent);
+ this.changeNum, patchNum, this.path, isOnParent, commentSide);
contentEl.appendChild(threadGroupEl);
}
- let threadEl = this._getThreadForRange(threadGroupEl, rangeToCheck);
+ let threadEl = this._getThread(threadGroupEl, opt_range);
if (!threadEl) {
- threadGroupEl.addNewThread(rangeToCheck, commentSide);
+ threadGroupEl.addNewThread(opt_range);
Polymer.dom.flush();
- threadEl = this._getThreadForRange(threadGroupEl, rangeToCheck);
- threadEl.commentSide = commentSide;
+ threadEl = this._getThread(threadGroupEl, opt_range);
}
return threadEl;
},
@@ -481,7 +504,7 @@
_handleThreadDiscard(e) {
const el = Polymer.dom(e).rootTarget;
- el.parentNode.removeThread(el.locationRange);
+ el.parentNode.removeThread(el.rootId);
},
_handleCommentDiscard(e) {
@@ -647,6 +670,7 @@
this.patchRange.patchNum,
this.path,
this._handleGetDiffError.bind(this)).then(diff => {
+ this._reportDiff(diff);
if (!this.commitRange) {
this.filesWeblinks = {};
return diff;
@@ -663,6 +687,38 @@
});
},
+ /**
+ * Report info about the diff response.
+ */
+ _reportDiff(diff) {
+ if (!diff || !diff.content) { return; }
+
+ // Count the delta lines stemming from normal deltas, and from
+ // due_to_rebase deltas.
+ let nonRebaseDelta = 0;
+ let rebaseDelta = 0;
+ diff.content.forEach(chunk => {
+ if (chunk.ab) { return; }
+ const deltaSize = Math.max(
+ chunk.a ? chunk.a.length : 0, chunk.b ? chunk.b.length : 0);
+ if (chunk.due_to_rebase) {
+ rebaseDelta += deltaSize;
+ } else {
+ nonRebaseDelta += deltaSize;
+ }
+ });
+
+ // Find the percent of the delta from due_to_rebase chunks rounded to two
+ // digits. Diffs with no delta are considered 0%.
+ const totalDelta = rebaseDelta + nonRebaseDelta;
+ const percentRebaseDelta = !totalDelta ? 0 :
+ Math.round(100 * rebaseDelta / totalDelta);
+
+ // Report the percentage in the "diff" category.
+ this.$.reporting.reportInteraction('rebase-delta-percent',
+ percentRebaseDelta);
+ },
+
/** @return {!Promise} */
_getLoggedIn() {
return this.$.restAPI.getLoggedIn();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 5b3670b..1f06dd5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -49,7 +49,7 @@
test('reload cancels before network resolves', () => {
element = fixture('basic');
- const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel');
+ const cancelStub = sandbox.stub(element, 'cancel');
// Stub the network calls into requests that never resolve.
sandbox.stub(element, '_getDiff', () => new Promise(() => {}));
@@ -58,13 +58,18 @@
assert.isTrue(cancelStub.called);
});
+ test('cancel', () => {
+ const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel');
+ element.cancel();
+ assert.isTrue(cancelStub.calledOnce);
+ });
+
test('_diffLength', () => {
element = fixture('basic');
const mock = document.createElement('mock-diff-response');
assert.equal(element._diffLength(mock.diffResponse), 52);
});
-
suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => {
let lineEl;
let contentEl;
@@ -191,7 +196,7 @@
test('loads files weblinks', () => {
const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks')
- .returns({name: 'stubb', url: '#s'});
+ .returns({name: 'stubb', url: '#s'});
sandbox.stub(element.$.restAPI, 'getDiff').returns(Promise.resolve({}));
element.projectName = 'test-project';
element.path = 'test-path';
@@ -353,18 +358,15 @@
element.patchRange = {basePatchNum: 1, patchNum: 2};
element.path = 'file.txt';
- sandbox.stub(element.$.diffBuilder, 'createCommentThreadGroup', () => {
- const threadGroup =
- document.createElement('gr-diff-comment-thread-group');
- threadGroup.patchForNewThreads = 1;
- return threadGroup;
- });
+ const mock = document.createElement('mock-diff-response');
+ element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder(
+ mock.diffResponse, {}, {tab_size: 2, line_length: 80});
// No thread groups.
assert.isNotOk(element._getThreadGroupForLine(contentEl));
// A thread group gets created.
- assert.isOk(element._getOrCreateThreadAtLineRange(contentEl,
+ assert.isOk(element._getOrCreateThread(contentEl,
patchNum, commentSide, side));
// Try to fetch a thread with a different range.
@@ -375,7 +377,7 @@
endChar: 3,
};
- assert.isOk(element._getOrCreateThreadAtLineRange(
+ assert.isOk(element._getOrCreateThread(
contentEl, patchNum, commentSide, side, range));
// The new thread group can be fetched.
assert.isOk(element._getThreadGroupForLine(contentEl));
@@ -833,6 +835,7 @@
getPreferences() {
return Promise.resolve({time_format: 'HHMM_12'});
},
+ getAccountCapabilities() { return Promise.resolve(); },
});
element = fixture('basic');
element.patchRange = {};
@@ -876,6 +879,24 @@
});
});
+ test('addDraftAtLine on an edit base', done => {
+ element.patchRange.patchNum = element.EDIT_NAME;
+ element.patchRange.basePatchNum = element.PARENT_NAME;
+ sandbox.stub(element, '_selectLine');
+ sandbox.stub(element, '_createComment');
+ const loggedInErrorSpy = sandbox.spy();
+ const alertSpy = sandbox.spy();
+ element.addEventListener('show-auth-required', loggedInErrorSpy);
+ element.addEventListener('show-alert', alertSpy);
+ element.addDraftAtLine(fakeLineEl);
+ flush(() => {
+ assert.isFalse(loggedInErrorSpy.called);
+ assert.isTrue(alertSpy.called);
+ assert.isFalse(element._createComment.called);
+ done();
+ });
+ });
+
suite('change in preferences', () => {
setup(() => {
element._diff = {
@@ -1112,6 +1133,81 @@
});
});
});
+
+ suite('_reportDiff', () => {
+ let reportStub;
+
+ setup(() => {
+ element = fixture('basic');
+ reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
+ });
+
+ test('null and content-less', () => {
+ element._reportDiff(null);
+ assert.isFalse(reportStub.called);
+
+ element._reportDiff({});
+ assert.isFalse(reportStub.called);
+ });
+
+ test('diff w/ no delta', () => {
+ const diff = {
+ content: [
+ {ab: ['foo', 'bar']},
+ {ab: ['baz', 'foo']},
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.strictEqual(reportStub.lastCall.args[1], 0);
+ });
+
+ test('diff w/ no rebase delta', () => {
+ const diff = {
+ content: [
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo']},
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo'], b: ['bar', 'baz']},
+ {ab: ['foo', 'bar']},
+ {b: ['baz', 'foo']},
+ {ab: ['foo', 'bar']},
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.strictEqual(reportStub.lastCall.args[1], 0);
+ });
+
+ test('diff w/ some rebase delta', () => {
+ const diff = {
+ content: [
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo'], due_to_rebase: true},
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo'], b: ['bar', 'baz']},
+ {ab: ['foo', 'bar']},
+ {b: ['baz', 'foo'], due_to_rebase: true},
+ {ab: ['foo', 'bar']},
+ {a: ['baz', 'foo']},
+ ],
+ };
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.strictEqual(reportStub.lastCall.args[1], 50);
+ });
+
+ test('diff w/ all rebase delta', () => {
+ const diff = {content: [{
+ a: ['foo', 'bar'],
+ b: ['baz', 'foo'],
+ due_to_rebase: true,
+ }]};
+ element._reportDiff(diff);
+ assert.isTrue(reportStub.calledOnce);
+ assert.strictEqual(reportStub.lastCall.args[1], 100);
+ });
+ });
});
a11ySuite('basic');
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-constants.html b/polygerrit-ui/app/elements/edit/gr-edit-constants.html
index 1941dc8..4bae900 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-constants.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-constants.html
@@ -19,8 +19,9 @@
const GrEditConstants = window.GrEditConstants || {};
+ // Order corresponds to order in the UI.
GrEditConstants.Actions = {
- EDIT: {label: 'Edit', id: 'edit'},
+ OPEN: {label: 'Open', id: 'open'},
DELETE: {label: 'Delete', id: 'delete'},
RENAME: {label: 'Rename', id: 'rename'},
RESTORE: {label: 'Restore', id: 'restore'},
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 5a59a37..2acbab5 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
@@ -69,6 +69,11 @@
padding: 0 .15em;
width: 100%;
}
+ @media screen and (max-width: 50em) {
+ gr-confirm-dialog {
+ width: 100vw;
+ }
+ }
</style>
<template is="dom-repeat" items="[[_actions]]" as="action">
<gr-button
@@ -79,13 +84,16 @@
</template>
<gr-overlay id="overlay" with-backdrop>
<gr-confirm-dialog
- id="editDialog"
+ id="openDialog"
class="invisible dialog"
disabled$="[[!_isValidPath(_path)]]"
- confirm-label="Edit"
- on-confirm="_handleEditConfirm"
+ confirm-label="Open"
+ confirm-on-enter
+ on-confirm="_handleOpenConfirm"
on-cancel="_handleDialogCancel">
- <div class="header" slot="header">Edit a file</div>
+ <div class="header" slot="header">
+ Open an existing or new file
+ </div>
<div class="main" slot="main">
<gr-autocomplete
placeholder="Enter an existing or new full file path."
@@ -98,9 +106,10 @@
class="invisible dialog"
disabled$="[[!_isValidPath(_path)]]"
confirm-label="Delete"
+ confirm-on-enter
on-confirm="_handleDeleteConfirm"
on-cancel="_handleDialogCancel">
- <div class="header" slot="header">Delete a file</div>
+ <div class="header" slot="header">Delete a file from the repo</div>
<div class="main" slot="main">
<gr-autocomplete
placeholder="Enter an existing full file path."
@@ -113,9 +122,10 @@
class="invisible dialog"
disabled$="[[!_computeRenameDisabled(_path, _newPath)]]"
confirm-label="Rename"
+ confirm-on-enter
on-confirm="_handleRenameConfirm"
on-cancel="_handleDialogCancel">
- <div class="header" slot="header">Rename a file</div>
+ <div class="header" slot="header">Rename a file in the repo</div>
<div class="main" slot="main">
<gr-autocomplete
placeholder="Enter an existing full file path."
@@ -132,6 +142,7 @@
id="restoreDialog"
class="invisible dialog"
confirm-label="Restore"
+ confirm-on-enter
on-confirm="_handleRestoreConfirm"
on-cancel="_handleDialogCancel">
<div class="header" slot="header">Restore this file?</div>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
index cffae06..e3b3494 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
@@ -59,8 +59,8 @@
e.preventDefault();
const action = Polymer.dom(e).localTarget.id;
switch (action) {
- case GrEditConstants.Actions.EDIT.id:
- this.openEditDialog();
+ case GrEditConstants.Actions.OPEN.id:
+ this.openOpenDialog();
return;
case GrEditConstants.Actions.DELETE.id:
this.openDeleteDialog();
@@ -74,21 +74,33 @@
}
},
- openEditDialog(opt_path) {
+ /**
+ * @param {string=} opt_path
+ */
+ openOpenDialog(opt_path) {
if (opt_path) { this._path = opt_path; }
- return this._showDialog(this.$.editDialog);
+ return this._showDialog(this.$.openDialog);
},
+ /**
+ * @param {string=} opt_path
+ */
openDeleteDialog(opt_path) {
if (opt_path) { this._path = opt_path; }
return this._showDialog(this.$.deleteDialog);
},
+ /**
+ * @param {string=} opt_path
+ */
openRenameDialog(opt_path) {
if (opt_path) { this._path = opt_path; }
return this._showDialog(this.$.renameDialog);
},
+ /**
+ * @param {string=} opt_path
+ */
openRestoreDialog(opt_path) {
if (opt_path) { this._path = opt_path; }
return this._showDialog(this.$.restoreDialog);
@@ -111,7 +123,7 @@
/**
* Given a dom event, gets the dialog that lies along this event path.
* @param {!Event} e
- * @return {!Element}
+ * @return {!Element|undefined}
*/
_getDialogFromEvent(e) {
return Polymer.dom(e).path.find(element => {
@@ -121,6 +133,11 @@
},
_showDialog(dialog) {
+ // Some dialogs may not fire their on-close event when closed in certain
+ // ways (e.g. by clicking outside the dialog body). This call prevents
+ // multiple dialogs from being shown in the same overlay.
+ this._hideAllDialogs();
+
return this.$.overlay.open().then(() => {
dialog.classList.toggle('invisible', false);
const autocomplete = dialog.querySelector('gr-autocomplete');
@@ -129,7 +146,18 @@
});
},
+ _hideAllDialogs() {
+ const dialogs = Polymer.dom(this.root).querySelectorAll('.dialog');
+ for (const dialog of dialogs) { this._closeDialog(dialog); }
+ },
+
+ /**
+ * @param {Element|undefined} dialog
+ * @param {boolean=} clearInputs
+ */
_closeDialog(dialog, clearInputs) {
+ if (!dialog) { return; }
+
if (clearInputs) {
// Dialog may have autocompletes and plain inputs -- as these have
// different properties representing their bound text, it is easier to
@@ -148,7 +176,7 @@
this._closeDialog(this._getDialogFromEvent(e));
},
- _handleEditConfirm(e) {
+ _handleOpenConfirm(e) {
const url = Gerrit.Nav.getEditUrlForDiff(this.change, this._path,
this.patchNum);
Gerrit.Nav.navigateToRelativeUrl(url);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
index 5dcc581..bbb5111 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
@@ -45,6 +45,7 @@
element.change = {_number: '42'};
showDialogSpy = sandbox.spy(element, '_showDialog');
closeDialogSpy = sandbox.spy(element, '_closeDialog');
+ sandbox.stub(element, '_hideAllDialogs');
queryStub = sandbox.stub(element.$.restAPI, 'queryChangeFiles')
.returns(Promise.resolve([]));
flushAsynchronousOperations();
@@ -75,17 +76,18 @@
assert.isTrue(element._isValidPath('test.js'));
});
- test('edit', () => {
- MockInteractions.tap(element.$$('#edit'));
+ test('open', () => {
+ MockInteractions.tap(element.$$('#open'));
element.patchNum = 1;
return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.editDialog.disabled);
+ assert.isTrue(element._hideAllDialogs.called);
+ assert.isTrue(element.$.openDialog.disabled);
assert.isFalse(queryStub.called);
- element.$.editDialog.querySelector('gr-autocomplete').text =
+ element.$.openDialog.querySelector('gr-autocomplete').text =
'src/test.cpp';
assert.isTrue(queryStub.called);
- assert.isFalse(element.$.editDialog.disabled);
- MockInteractions.tap(element.$.editDialog.$$('gr-button[primary]'));
+ assert.isFalse(element.$.openDialog.disabled);
+ MockInteractions.tap(element.$.openDialog.$$('gr-button[primary]'));
for (const stub of navStubs) { assert.isTrue(stub.called); }
assert.deepEqual(Gerrit.Nav.getEditUrlForDiff.lastCall.args,
[element.change, 'src/test.cpp', element.patchNum]);
@@ -94,13 +96,13 @@
});
test('cancel', () => {
- MockInteractions.tap(element.$$('#edit'));
+ MockInteractions.tap(element.$$('#open'));
return showDialogSpy.lastCall.returnValue.then(() => {
- assert.isTrue(element.$.editDialog.disabled);
- element.$.editDialog.querySelector('gr-autocomplete').text =
+ assert.isTrue(element.$.openDialog.disabled);
+ element.$.openDialog.querySelector('gr-autocomplete').text =
'src/test.cpp';
- assert.isFalse(element.$.editDialog.disabled);
- MockInteractions.tap(element.$.editDialog.$$('gr-button'));
+ assert.isFalse(element.$.openDialog.disabled);
+ MockInteractions.tap(element.$.openDialog.$$('gr-button'));
for (const stub of navStubs) { assert.isFalse(stub.called); }
assert.isTrue(closeDialogSpy.called);
assert.equal(element._path, 'src/test.cpp');
@@ -319,10 +321,10 @@
});
});
- test('openEditDialog', () => {
- return element.openEditDialog('test/path.cpp').then(() => {
- assert.isFalse(element.$.editDialog.hasAttribute('hidden'));
- assert.equal(element.$.editDialog.querySelector('gr-autocomplete').text,
+ test('openOpenDialog', () => {
+ return element.openOpenDialog('test/path.cpp').then(() => {
+ assert.isFalse(element.$.openDialog.hasAttribute('hidden'));
+ assert.equal(element.$.openDialog.querySelector('gr-autocomplete').text,
'test/path.cpp');
});
});
@@ -331,9 +333,9 @@
const spy = sandbox.spy(element, '_getDialogFromEvent');
element.addEventListener('tap', element._getDialogFromEvent);
- MockInteractions.tap(element.$.editDialog);
+ MockInteractions.tap(element.$.openDialog);
flushAsynchronousOperations();
- assert.equal(spy.lastCall.returnValue.id, 'editDialog');
+ assert.equal(spy.lastCall.returnValue.id, 'openDialog');
MockInteractions.tap(element.$.deleteDialog);
flushAsynchronousOperations();
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 96ba196b..f46f414 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
@@ -30,11 +30,7 @@
display: flex;
justify-content: flex-end;
}
- #edit {
- text-decoration: none;
- }
- #edit,
- #more {
+ #actions {
margin-right: 1em;
}
gr-button,
@@ -53,18 +49,13 @@
}
}
</style>
- <gr-button
- id="edit"
- link
- on-tap="_handleEditTap">Edit</gr-button>
- <!-- TODO(kaspern): implement more menu. -->
<gr-dropdown
- id="more"
+ id="actions"
items="[[_fileActions]]"
down-arrow
vertical-offset="20"
on-tap-item="_handleActionTap"
- link>More</gr-dropdown>
+ link>Actions</gr-dropdown>
</template>
<script src="gr-edit-file-controls.js"></script>
</dom-module>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
index 62a4785..6a0ccf5 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
@@ -25,11 +25,9 @@
properties: {
filePath: String,
- // Edit action not needed in the overflow.
_allFileActions: {
type: Array,
- value: () => Object.values(GrEditConstants.Actions)
- .filter(action => action !== GrEditConstants.Actions.EDIT),
+ value: () => Object.values(GrEditConstants.Actions),
},
_fileActions: {
type: Array,
@@ -37,12 +35,6 @@
},
},
- _handleEditTap(e) {
- e.preventDefault();
- e.stopPropagation();
- this._dispatchFileAction(GrEditConstants.Actions.EDIT.id, this.filePath);
- },
-
_handleActionTap(e) {
e.preventDefault();
e.stopPropagation();
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
index 42fb466..27b28fa 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
@@ -47,55 +47,56 @@
teardown(() => { sandbox.restore(); });
- test('edit tap emits event', () => {
+ test('open tap emits event', () => {
+ const actions = element.$.actions;
element.filePath = 'foo';
+ actions._open();
+ flushAsynchronousOperations();
- MockInteractions.tap(element.$.edit);
+ MockInteractions.tap(actions.$$('li [data-id="open"]'));
assert.isTrue(fileActionHandler.called);
assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
- {action: GrEditConstants.Actions.EDIT.id, path: 'foo'});
+ {action: GrEditConstants.Actions.OPEN.id, path: 'foo'});
});
test('delete tap emits event', () => {
- const more = element.$.more;
+ const actions = element.$.actions;
element.filePath = 'foo';
- more._open();
+ actions._open();
flushAsynchronousOperations();
- MockInteractions.tap(more.$$('li [data-id="delete"]'));
+ MockInteractions.tap(actions.$$('li [data-id="delete"]'));
assert.isTrue(fileActionHandler.called);
assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
{action: GrEditConstants.Actions.DELETE.id, path: 'foo'});
});
test('restore tap emits event', () => {
- const more = element.$.more;
+ const actions = element.$.actions;
element.filePath = 'foo';
- more._open();
+ actions._open();
flushAsynchronousOperations();
- MockInteractions.tap(more.$$('li [data-id="restore"]'));
+ MockInteractions.tap(actions.$$('li [data-id="restore"]'));
assert.isTrue(fileActionHandler.called);
assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
{action: GrEditConstants.Actions.RESTORE.id, path: 'foo'});
});
test('rename tap emits event', () => {
- const more = element.$.more;
+ const actions = element.$.actions;
element.filePath = 'foo';
- more._open();
+ actions._open();
flushAsynchronousOperations();
- MockInteractions.tap(more.$$('li [data-id="rename"]'));
+ MockInteractions.tap(actions.$$('li [data-id="rename"]'));
assert.isTrue(fileActionHandler.called);
assert.deepEqual(fileActionHandler.lastCall.args[0].detail,
{action: GrEditConstants.Actions.RENAME.id, path: 'foo'});
});
test('computed properties', () => {
- assert.equal(element._allFileActions.length, 3);
- assert.notOk(element._allFileActions
- .find(action => action.id === GrEditConstants.Actions.EDIT.id));
+ assert.equal(element._allFileActions.length, 4);
});
});
</script>
\ No newline at end of file
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 ac8ec08..8f2f166 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
@@ -36,8 +36,8 @@
background-color: var(--view-background-color);
}
gr-fixed-panel {
- background-color: #fff;
- border-bottom: 1px #eee solid;
+ background-color: #ebf5fb;
+ border-bottom: 1px #ddd solid;
z-index: 1;
}
header,
@@ -48,7 +48,7 @@
padding: .75em var(--default-horizontal-margin);
}
header gr-editable-label {
- font-size: 1.2rem;
+ font-size: var(--font-size-large);
font-weight: bold;
--label-style: {
text-overflow: initial;
@@ -67,6 +67,11 @@
.textareaWrapper .editButtons {
display: none;
}
+ .controlGroup {
+ align-items: center;
+ display: flex;
+ font-size: var(--font-size-large);
+ }
.rightControls {
justify-content: flex-end;
}
@@ -82,12 +87,16 @@
</style>
<gr-fixed-panel keep-on-scroll>
<header>
- <gr-editable-label
- label-text="File path"
- value="[[_path]]"
- placeholder="File path..."
- on-changed="_handlePathChanged"></gr-editable-label>
- <span class="rightControls">
+ <span class="controlGroup">
+ <span>Edit mode</span>
+ <span class="separator"></span>
+ <gr-editable-label
+ label-text="File path"
+ value="[[_path]]"
+ placeholder="File path..."
+ on-changed="_handlePathChanged"></gr-editable-label>
+ </span>
+ <span class="controlGroup rightControls">
<gr-button
id="close"
link
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 866a62b..bd07375 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
@@ -135,17 +135,24 @@
_viewEditInChangeView() {
const patch = this._successfulSave ? this.EDIT_NAME : this._patchNum;
- Gerrit.Nav.navigateToChange(this._change, patch);
+ Gerrit.Nav.navigateToChange(this._change, patch, null,
+ patch !== this.EDIT_NAME);
},
_getFileData(changeNum, path, patchNum) {
return this.$.restAPI.getFileContent(changeNum, path, patchNum)
.then(res => {
- if (!res.ok) { return; }
-
- this._type = res.type || '';
this._newContent = res.content || '';
this._content = res.content || '';
+
+ // A non-ok response may result if the file does not yet exist.
+ // The `type` field of the response is only valid when the file
+ // already exists.
+ if (res.ok && res.type) {
+ this._type = res.type;
+ } else {
+ this._type = '';
+ }
});
},
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 18ff1f5..ea87f56 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
@@ -242,9 +242,9 @@
// Ensure no data is set with a bad response.
return element._getFileData('1', 'test/path', 'edit').then(() => {
- assert.equal(element._newContent, 'initial');
- assert.equal(element._content, 'initial');
- assert.equal(element._type, 'initial');
+ assert.equal(element._newContent, '');
+ assert.equal(element._content, '');
+ assert.equal(element._type, '');
});
});
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 68b9b59..46f4649 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -78,13 +78,14 @@
footer {
color: var(--primary-text-color);
}
- gr-main-header.shadow {
- /* Make it obvious for shadow dom testing */
- border-bottom: 1px solid pink;
- }
gr-main-header {
background-color: var(--header-background-color);
padding: 0 var(--default-horizontal-margin);
+ border-bottom: 1px solid #ddd;
+ }
+ gr-main-header.shadow {
+ /* Make it obvious for shadow dom testing */
+ border-bottom: 1px solid pink;
}
footer {
background-color: var(--footer-background-color);
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
index da9cae6..00735cf 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
@@ -82,16 +82,6 @@
</span>
</section>
<section>
- <span class="title">Hide line numbers</span>
- <span class="value">
- <input
- id="hideLineNumbers"
- type="checkbox"
- checked$="[[editPrefs.hide_line_numbers]]"
- on-change="_handleLineNumbersChanged">
- </span>
- </section>
- <section>
<span class="title">Match brackets</span>
<span class="value">
<input
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
index 5a97363..dbacfda 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -49,11 +49,6 @@
this._handleEditPrefsChanged();
},
- _handleLineNumbersChanged() {
- this.set('editPrefs.hide_line_numbers', this.$.hideLineNumbers.checked);
- this._handleEditPrefsChanged();
- },
-
_handleMatchBracketsChanged() {
this.set('editPrefs.match_brackets', this.$.showMatchBrackets.checked);
this._handleEditPrefsChanged();
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
index a0f95c1..e91290f6 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
@@ -93,8 +93,6 @@
.firstElementChild.checked, editPreferences.syntax_highlighting);
assert.equal(valueOf('Show tabs', 'editPreferences')
.firstElementChild.checked, editPreferences.show_tabs);
- assert.equal(valueOf('Hide line numbers', 'editPreferences')
- .firstElementChild.checked, editPreferences.hide_line_numbers);
assert.equal(valueOf('Match brackets', 'editPreferences')
.firstElementChild.checked, editPreferences.match_brackets);
assert.equal(valueOf('Line wrapping', 'editPreferences')
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
new file mode 100644
index 0000000..b38509d
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.html
@@ -0,0 +1,137 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../styles/gr-form-styles.html">
+<link rel="import" href="../../shared/gr-button/gr-button.html">
+<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+
+<dom-module id="gr-gpg-editor">
+ <template>
+ <style include="shared-styles"></style>
+ <style include="gr-form-styles">
+ .statusHeader {
+ width: 4em;
+ }
+ .keyHeader {
+ width: 9em;
+ }
+ .userIdHeader {
+ width: 15em;
+ }
+ #viewKeyOverlay {
+ padding: 2em;
+ width: 50em;
+ }
+ .publicKey {
+ font-family: var(--monospace-font-family);
+ overflow-x: scroll;
+ overflow-wrap: break-word;
+ width: 30em;
+ }
+ .closeButton {
+ bottom: 2em;
+ position: absolute;
+ right: 2em;
+ }
+ #existing {
+ margin-bottom: 1em;
+ }
+ #existing .commentColumn {
+ min-width: 27em;
+ width: auto;
+ }
+ </style>
+ <div class="gr-form-styles">
+ <fieldset id="existing">
+ <table>
+ <thead>
+ <tr>
+ <th class="idColumn">ID</th>
+ <th class="fingerPrintColumn">Fingerprint</th>
+ <th class="userIdHeader">User IDs</th>
+ <th class="keyHeader">Public Key</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template is="dom-repeat" items="[[_keys]]" as="key">
+ <tr>
+ <td class="idColumn">[[key.id]]</td>
+ <td class="fingerPrintColumn">[[key.fingerprint]]</td>
+ <td class="userIdHeader">
+ <template is="dom-repeat" items="[[key.user_ids]]">
+ [[item]]
+ </template>
+ </td>
+ <td class="keyHeader">
+ <gr-button
+ on-tap="_showKey"
+ data-index$="[[index]]"
+ link>Click to View</gr-button>
+ </td>
+ <td>
+ <gr-button
+ data-index$="[[index]]"
+ on-tap="_handleDeleteKey">Delete</gr-button>
+ </td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ <gr-overlay id="viewKeyOverlay" with-backdrop>
+ <fieldset>
+ <section>
+ <span class="title">Status</span>
+ <span class="value">[[_keyToView.status]]</span>
+ </section>
+ <section>
+ <span class="title">Key</span>
+ <span class="value">[[_keyToView.key]]</span>
+ </section>
+ </fieldset>
+ <gr-button
+ class="closeButton"
+ on-tap="_closeOverlay">Close</gr-button>
+ </gr-overlay>
+ <gr-button
+ on-tap="save"
+ disabled$="[[!hasUnsavedChanges]]">Save changes</gr-button>
+ </fieldset>
+ <fieldset>
+ <section>
+ <span class="title">New GPG key</span>
+ <span class="value">
+ <iron-autogrow-textarea
+ id="newKey"
+ autocomplete="on"
+ bind-value="{{_newKey}}"
+ placeholder="New GPG Key"></iron-autogrow-textarea>
+ </span>
+ </section>
+ <gr-button
+ id="addButton"
+ disabled$="[[_computeAddButtonDisabled(_newKey)]]"
+ on-tap="_handleAddKey">Add new GPG key</gr-button>
+ </fieldset>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </template>
+ <script src="gr-gpg-editor.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
new file mode 100644
index 0000000..f5bf8bc
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
@@ -0,0 +1,102 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-gpg-editor',
+
+ properties: {
+ hasUnsavedChanges: {
+ type: Boolean,
+ value: false,
+ notify: true,
+ },
+ _keys: Array,
+ /** @type {?} */
+ _keyToView: Object,
+ _newKey: {
+ type: String,
+ value: '',
+ },
+ _keysToRemove: {
+ type: Array,
+ value() { return []; },
+ },
+ },
+
+ loadData() {
+ this._keys = [];
+ return this.$.restAPI.getAccountGPGKeys().then(keys => {
+ if (!keys) {
+ return;
+ }
+ this._keys = Object.keys(keys)
+ .map(key => {
+ const gpgKey = keys[key];
+ gpgKey.id = key;
+ return gpgKey;
+ });
+ });
+ },
+
+ save() {
+ const promises = this._keysToRemove.map(key => {
+ this.$.restAPI.deleteAccountGPGKey(key.id);
+ });
+
+ return Promise.all(promises).then(() => {
+ this._keysToRemove = [];
+ this.hasUnsavedChanges = false;
+ });
+ },
+
+ _showKey(e) {
+ const el = Polymer.dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this._keyToView = this._keys[index];
+ this.$.viewKeyOverlay.open();
+ },
+
+ _closeOverlay() {
+ this.$.viewKeyOverlay.close();
+ },
+
+ _handleDeleteKey(e) {
+ const el = Polymer.dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this.push('_keysToRemove', this._keys[index]);
+ this.splice('_keys', index, 1);
+ this.hasUnsavedChanges = true;
+ },
+
+ _handleAddKey() {
+ this.$.addButton.disabled = true;
+ this.$.newKey.disabled = true;
+ return this.$.restAPI.addAccountGPGKey({add: [this._newKey.trim()]})
+ .then(key => {
+ this.$.newKey.disabled = false;
+ this._newKey = '';
+ this.loadData();
+ }).catch(() => {
+ this.$.addButton.disabled = false;
+ this.$.newKey.disabled = false;
+ });
+ },
+
+ _computeAddButtonDisabled(newKey) {
+ return !newKey.length;
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
new file mode 100644
index 0000000..f749130
--- /dev/null
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
@@ -0,0 +1,192 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-gpg-editor</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-gpg-editor.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-gpg-editor></gr-gpg-editor>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-gpg-editor tests', () => {
+ let element;
+ let keys;
+
+ setup(done => {
+ const fingerprint1 = '0192 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B';
+ const fingerprint2 = '0196 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B';
+ keys = {
+ AFC8A49B: {
+ fingerprint: fingerprint1,
+ user_ids: [
+ 'John Doe john.doe@example.com',
+ ],
+ key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+ '\nVersion: BCPG v1.52\n\t<key 1>',
+ status: 'TRUSTED',
+ problems: [],
+ },
+ AED9B59C: {
+ fingerprint: fingerprint2,
+ user_ids: [
+ 'Gerrit gerrit@example.com',
+ ],
+ key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+ '\nVersion: BCPG v1.52\n\t<key 2>',
+ status: 'TRUSTED',
+ problems: [],
+ },
+ };
+
+ stub('gr-rest-api-interface', {
+ getAccountGPGKeys() { return Promise.resolve(keys); },
+ });
+
+ element = fixture('basic');
+
+ element.loadData().then(() => { flush(done); });
+ });
+
+ test('renders', () => {
+ const rows = Polymer.dom(element.root).querySelectorAll('tbody tr');
+
+ assert.equal(rows.length, 2);
+
+ let cells = rows[0].querySelectorAll('td');
+ assert.equal(cells[0].textContent, 'AFC8A49B');
+
+ cells = rows[1].querySelectorAll('td');
+ assert.equal(cells[0].textContent, 'AED9B59C');
+ });
+
+ test('remove key', done => {
+ const lastKey = keys[Object.keys(keys)[1]];
+
+ const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountGPGKey',
+ () => { return Promise.resolve(); });
+
+ assert.equal(element._keysToRemove.length, 0);
+ assert.isFalse(element.hasUnsavedChanges);
+
+ // Get the delete button for the last row.
+ const button = Polymer.dom(element.root).querySelector(
+ 'tbody tr:last-of-type td:nth-child(5) gr-button');
+
+ MockInteractions.tap(button);
+
+ assert.equal(element._keys.length, 1);
+ assert.equal(element._keysToRemove.length, 1);
+ assert.equal(element._keysToRemove[0], lastKey);
+ assert.isTrue(element.hasUnsavedChanges);
+ assert.isFalse(saveStub.called);
+
+ element.save().then(() => {
+ assert.isTrue(saveStub.called);
+ assert.equal(saveStub.lastCall.args[0], Object.keys(keys)[1]);
+ assert.equal(element._keysToRemove.length, 0);
+ assert.isFalse(element.hasUnsavedChanges);
+ done();
+ });
+ });
+
+ test('show key', () => {
+ const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open');
+
+ // Get the show button for the last row.
+ const button = Polymer.dom(element.root).querySelector(
+ 'tbody tr:last-of-type td:nth-child(4) gr-button');
+
+ MockInteractions.tap(button);
+
+ assert.equal(element._keyToView, keys[Object.keys(keys)[1]]);
+ assert.isTrue(openSpy.called);
+ });
+
+ test('add key', done => {
+ const newKeyString =
+ '-----BEGIN PGP PUBLIC KEY BLOCK-----' +
+ '\nVersion: BCPG v1.52\n\t<key 3>';
+ const newKeyObject = {
+ ADE8A59B: {
+ fingerprint: '0194 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B',
+ user_ids: [
+ 'John john@example.com',
+ ],
+ key: newKeyString,
+ status: 'TRUSTED',
+ problems: [],
+ },
+ };
+
+ const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
+ () => { return Promise.resolve(newKeyObject); });
+
+ element._newKey = newKeyString;
+
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+
+ element._handleAddKey().then(() => {
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+ assert.equal(element._keys.length, 2);
+ done();
+ });
+
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isTrue(element.$.newKey.disabled);
+
+ assert.isTrue(addStub.called);
+ assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+ });
+
+ test('add invalid key', done => {
+ const newKeyString = 'not even close to valid';
+
+ const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
+ () => { return Promise.reject(); });
+
+ element._newKey = newKeyString;
+
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+
+ element._handleAddKey().then(() => {
+ assert.isFalse(element.$.addButton.disabled);
+ assert.isFalse(element.$.newKey.disabled);
+ assert.equal(element._keys.length, 2);
+ done();
+ });
+
+ assert.isTrue(element.$.addButton.disabled);
+ assert.isTrue(element.$.newKey.disabled);
+
+ assert.isTrue(addStub.called);
+ assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]});
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
index c70ae88..5964578 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
@@ -32,7 +32,7 @@
</test-fixture>
<script>
- suite('gr-settings-view tests', () => {
+ suite('gr-menu-editor tests', () => {
let element;
let menu;
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 a1de1b2..416c426 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
@@ -31,6 +31,7 @@
<link rel="import" href="../gr-agreements-list/gr-agreements-list.html">
<link rel="import" href="../gr-edit-preferences/gr-edit-preferences.html">
<link rel="import" href="../gr-email-editor/gr-email-editor.html">
+<link rel="import" href="../gr-gpg-editor/gr-gpg-editor.html">
<link rel="import" href="../gr-group-list/gr-group-list.html">
<link rel="import" href="../gr-http-password/gr-http-password.html">
<link rel="import" href="../gr-identities/gr-identities.html">
@@ -73,6 +74,9 @@
<li hidden$="[[!_serverConfig.sshd]]"><a href="#SSHKeys">
SSH Keys
</a></li>
+ <li hidden$="[[!_serverConfig.receive.enable_signed_push]]"><a href="#GPGKeys">
+ GPG Keys
+ </a></li>
<li><a href="#Groups">Groups</a></li>
<li><a href="#Identities">Identities</a></li>
<template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]">
@@ -414,6 +418,14 @@
id="sshEditor"
has-unsaved-changes="{{_keysChanged}}"></gr-ssh-editor>
</div>
+ <div hidden$="[[!_serverConfig.receive.enable_signed_push]]">
+ <h2
+ id="GPGKeys"
+ class$="[[_computeHeaderClass(_gpgKeysChanged)]]">GPG keys</h2>
+ <gr-gpg-editor
+ id="gpgEditor"
+ has-unsaved-changes="{{_gpgKeysChanged}}"></gr-gpg-editor>
+ </div>
<h2 id="Groups">Groups</h2>
<fieldset>
<gr-group-list id="groupList"></gr-group-list>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 8e14018..912712c 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -104,6 +104,10 @@
type: Boolean,
value: false,
},
+ _gpgKeysChanged: {
+ type: Boolean,
+ value: false,
+ },
_newEmail: String,
_addingEmail: {
type: Boolean,
@@ -167,10 +171,16 @@
this._serverConfig = config;
const configPromises = [];
- if (this._serverConfig.sshd) {
+ if (this._serverConfig && this._serverConfig.sshd) {
configPromises.push(this.$.sshEditor.loadData());
}
+ if (this._serverConfig &&
+ this._serverConfig.receive &&
+ this._serverConfig.receive.enable_signed_push) {
+ configPromises.push(this.$.gpgEditor.loadData());
+ }
+
configPromises.push(
this.getDocsBaseUrl(config, this.$.restAPI)
.then(baseUrl => { this._docsBaseUrl = baseUrl; }));
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 52fc69b..de92b97 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
@@ -17,6 +17,7 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../gr-account-link/gr-account-link.html">
<link rel="import" href="../gr-button/gr-button.html">
+<link rel="import" href="../gr-icons/gr-icons.html">
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
<link rel="import" href="../../../styles/shared-styles.html">
@@ -52,7 +53,6 @@
height: .6em;
line-height: .6;
margin-left: .15em;
- margin-top: -.05em;
padding: 0;
text-decoration: none;
}
@@ -74,6 +74,10 @@
opacity: .6;
pointer-events: none;
}
+ iron-icon {
+ height: 1.2rem;
+ width: 1.2rem;
+ }
</style>
<div class$="container [[_getBackgroundClass(transparentBackground)]]">
<gr-account-link account="[[account]]"
@@ -87,7 +91,9 @@
tabindex="-1"
aria-label="Remove"
class$="remove [[_getBackgroundClass(transparentBackground)]]"
- on-tap="_handleRemoveTap">ร</gr-button>
+ on-tap="_handleRemoveTap">
+ <iron-icon icon="gr-icons:close"></iron-icon>
+ </gr-button>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
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 6801dea..968143a 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
@@ -54,12 +54,12 @@
justify-content: flex-end;
}
</style>
- <div class="container">
+ <div class="container" on-keydown="_handleKeydown">
<header><slot name="header"></slot></header>
<main><slot name="main"></slot></main>
<footer>
<gr-button link on-tap="_handleCancelTap">[[cancelLabel]]</gr-button>
- <gr-button link primary on-tap="_handleConfirmTap" disabled="[[disabled]]">
+ <gr-button link primary on-tap="_handleConfirm" disabled="[[disabled]]">
[[confirmLabel]]
</gr-button>
</footer>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
index f322e25..8916296 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
@@ -42,13 +42,19 @@
type: Boolean,
value: false,
},
+ confirmOnEnter: {
+ type: Boolean,
+ value: false,
+ },
},
hostAttributes: {
role: 'dialog',
},
- _handleConfirmTap(e) {
+ _handleConfirm(e) {
+ if (this.disabled) { return; }
+
e.preventDefault();
this.fire('confirm', null, {bubbles: false});
},
@@ -57,5 +63,9 @@
e.preventDefault();
this.fire('cancel', null, {bubbles: false});
},
+
+ _handleKeydown(e) {
+ if (this.confirmOnEnter && e.keyCode === 13) { this._handleConfirm(e); }
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
index 309e15c..a2ae160 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
@@ -34,11 +34,15 @@
<script>
suite('gr-confirm-dialog tests', () => {
let element;
+ let sandbox;
setup(() => {
+ sandbox = sinon.sandbox.create();
element = fixture('basic');
});
+ teardown(() => { sandbox.restore(); });
+
test('events', done => {
let numEvents = 0;
function handler() { if (++numEvents == 2) { done(); } }
@@ -49,5 +53,24 @@
MockInteractions.tap(element.$$('gr-button[primary]'));
MockInteractions.tap(element.$$('gr-button:not([primary])'));
});
+
+ test('confirmOnEnter', () => {
+ element.confirmOnEnter = false;
+ const handleConfirmStub = sandbox.stub(element, '_handleConfirm');
+ const handleKeydownSpy = sandbox.spy(element, '_handleKeydown');
+ MockInteractions.pressAndReleaseKeyOn(element.$$('main'),
+ 13, null, 'enter');
+ flushAsynchronousOperations();
+
+ assert.isTrue(handleKeydownSpy.called);
+ assert.isFalse(handleConfirmStub.called);
+
+ element.confirmOnEnter = true;
+ MockInteractions.pressAndReleaseKeyOn(element.$$('main'),
+ 13, null, 'enter');
+ flushAsynchronousOperations();
+
+ assert.isTrue(handleConfirmStub.called);
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
index 34a5af5..06aa43b 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.html
@@ -33,6 +33,12 @@
<g id="star"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></g>
<!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
<g id="star-border"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/></g>
+ <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+ <g id="close"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></g>
+ <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+ <g id="chevron-left"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></g>
+ <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.html -->
+ <g id="chevron-right"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></g>
<!-- This is a custom PolyGerrit SVG -->
<g id="side-by-side"><path d="M17.1578947,10.8888889 L2.84210526,10.8888889 C2.37894737,10.8888889 2,11.2888889 2,11.7777778 L2,17.1111111 C2,17.6 2.37894737,18 2.84210526,18 L17.1578947,18 C17.6210526,18 18,17.6 18,17.1111111 L18,11.7777778 C18,11.2888889 17.6210526,10.8888889 17.1578947,10.8888889 Z M17.1578947,2 L2.84210526,2 C2.37894737,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.37894737,9.11111111 2.84210526,9.11111111 L17.1578947,9.11111111 C17.6210526,9.11111111 18,8.71111111 18,8.22222222 L18,2.88888889 C18,2.4 17.6210526,2 17.1578947,2 Z M16.1973628,2 L2.78874238,2 C2.35493407,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.35493407,9.11111111 2.78874238,9.11111111 L16.1973628,9.11111111 C16.6311711,9.11111111 16.9861052,8.71111111 16.9861052,8.22222222 L16.9861052,2.88888889 C16.9861052,2.4 16.6311711,2 16.1973628,2 Z" id="Shape" transform="scale(1.2) translate(10.000000, 10.000000) rotate(-90.000000) translate(-10.000000, -10.000000)"/></g>
<!-- This is a custom PolyGerrit SVG -->
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 e56498b..ca0b90a 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
@@ -17,6 +17,7 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html">
<link rel="import" href="../gr-button/gr-button.html">
+<link rel="import" href="../gr-icons/gr-icons.html">
<link rel="import" href="../gr-limited-text/gr-limited-text.html">
<link rel="import" href="../../../styles/shared-styles.html">
@@ -49,7 +50,6 @@
height: .6em;
line-height: .6;
margin-left: .15em;
- margin-top: -.05em;
padding: 0;
text-decoration: none;
}
@@ -65,6 +65,10 @@
a {
color: var(--linked-chip-text-color);
}
+ iron-icon {
+ height: 1.2rem;
+ width: 1.2rem;
+ }
</style>
<div class$="container [[_getBackgroundClass(transparentBackground)]]">
<a href$="[[href]]">
@@ -78,7 +82,9 @@
hidden$="[[!removable]]"
hidden
class$="remove [[_getBackgroundClass(transparentBackground)]]"
- on-tap="_handleRemoveTap">ร</gr-button>
+ on-tap="_handleRemoveTap">
+ <iron-icon icon="gr-icons:close"></iron-icon>
+ </gr-button>
</div>
</template>
<script src="gr-linked-chip.js"></script>
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 cbb2287..27f330e 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
@@ -437,12 +437,16 @@
.then(response => this.getResponseObject(response));
},
- saveIncludedGroup(groupName, includedGroup) {
+ saveIncludedGroup(groupName, includedGroup, opt_errFn) {
const encodeName = encodeURIComponent(groupName);
const encodeIncludedGroup = encodeURIComponent(includedGroup);
return this.send('PUT',
- `/groups/${encodeName}/groups/${encodeIncludedGroup}`)
- .then(response => this.getResponseObject(response));
+ `/groups/${encodeName}/groups/${encodeIncludedGroup}`, null,
+ opt_errFn).then(response => {
+ if (response.ok) {
+ return this.getResponseObject(response);
+ }
+ });
},
deleteGroupMembers(groupName, groupMembers) {
@@ -1418,9 +1422,15 @@
* @param {number|string} patchNum
*/
getFileContent(changeNum, path, patchNum) {
+ // 404s indicate the file does not exist yet in the revision, so suppress
+ // them.
+ const suppress404s = res => {
+ if (res && res.status !== 404) { this.fire('server-error', {res}); }
+ return res;
+ };
const promise = this.patchNumEquals(patchNum, this.EDIT_NAME) ?
this._getFileInChangeEdit(changeNum, path) :
- this._getFileInRevision(changeNum, path, patchNum);
+ this._getFileInRevision(changeNum, path, patchNum, suppress404s);
return promise.then(res => {
if (!res.ok) { return res; }
@@ -1439,12 +1449,13 @@
* @param {number|string} changeNum
* @param {string} path
* @param {number|string} patchNum
+ * @param {?function(?Response, string=)=} opt_errFn
*/
- _getFileInRevision(changeNum, path, patchNum) {
+ _getFileInRevision(changeNum, path, patchNum, opt_errFn) {
const e = `/files/${encodeURIComponent(path)}/content`;
const headers = {Accept: 'application/json'};
- return this.getChangeURLAndSend(changeNum, 'GET', patchNum, e, null, null,
- null, null, headers);
+ return this.getChangeURLAndSend(changeNum, 'GET', patchNum, e, null,
+ opt_errFn, null, null, headers);
},
/**
@@ -1941,6 +1952,28 @@
return this.send('DELETE', '/accounts/self/sshkeys/' + id);
},
+ getAccountGPGKeys() {
+ return this.fetchJSON('/accounts/self/gpgkeys');
+ },
+
+ addAccountGPGKey(key) {
+ return this.send('POST', '/accounts/self/gpgkeys', key)
+ .then(response => {
+ if (response.status < 200 && response.status >= 300) {
+ return Promise.reject();
+ }
+ return this.getResponseObject(response);
+ })
+ .then(obj => {
+ if (!obj) { return Promise.reject(); }
+ return obj;
+ });
+ },
+
+ deleteAccountGPGKey(id) {
+ return this.send('DELETE', '/accounts/self/gpgkeys/' + id);
+ },
+
deleteVote(changeNum, account, label) {
const e = `/reviewers/${account}/votes/${encodeURIComponent(label)}`;
return this.getChangeURLAndSend(changeNum, 'DELETE', null, e);
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 efe70a9..352f759 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
@@ -1277,6 +1277,23 @@
return Promise.all([edit, normal]);
});
+ test('getFileContent suppresses 404s', done => {
+ const res = {status: 404};
+ const handler = e => {
+ assert.isFalse(e.detail.res.status === 404);
+ done();
+ };
+ element.addEventListener('server-error', handler);
+ sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve(res));
+ sandbox.stub(element, '_changeBaseURL').returns(Promise.resolve(''));
+ element.getFileContent('1', 'tst/path', '1').then(() => {
+ flushAsynchronousOperations();
+
+ res.status = 500;
+ element.getFileContent('1', 'tst/path', '1');
+ });
+ });
+
test('getChangeFilesAsSpeciallySortedArray is edit-sensitive', () => {
const fn = element.getChangeFilesAsSpeciallySortedArray.bind(element);
const getChangeFilesStub = sandbox.stub(element, 'getChangeFiles')
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 145363b..0d03910 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -16,7 +16,7 @@
# See: https://github.com/google/closure-compiler/issues/2042
compilation_level = "WHITESPACE_ONLY",
defs = [
- "--polymer_pass",
+ "--polymer_version=1",
"--jscomp_off=duplicate",
"--force_inject_library=es6_runtime",
],
diff --git a/polygerrit-ui/app/styles/gr-change-list-styles.html b/polygerrit-ui/app/styles/gr-change-list-styles.html
index 566d098..c109381 100644
--- a/polygerrit-ui/app/styles/gr-change-list-styles.html
+++ b/polygerrit-ui/app/styles/gr-change-list-styles.html
@@ -19,13 +19,12 @@
:host {
font-size: var(--font-size-normal);
}
- gr-change-list-item:not([selected]),
+ gr-change-list-item,
tr {
- border-top: 1px solid #ddd;
+ border-bottom: 1px solid #ddd;
}
- tbody {
- border: 1px solid #ddd;
- border-top: none;
+ tr.topHeader {
+ border: none;
}
th {
text-align: left;
@@ -38,17 +37,38 @@
.cell:not(.label) {
padding-right: 8px;
}
+ th.label {
+ border-left: none;
+ }
.topHeader,
.groupHeader {
font-family: var(--font-family-bold);
}
- .topHeader {
+ .topHeader th {
background-color: #fafafa;
font-size: var(--font-size-large);
height: 3rem;
+ position: -webkit-sticky;
+ position: sticky;
+ top: -1px; /* Offset for top borders */
+ z-index: 1;
+ }
+ /* :after pseudoelements are used here because borders on sticky table
+ headers with a background color are broken. */
+ th:after {
+ border-bottom: 1px solid #ddd;
+ bottom: 0;
+ content: '';
+ left: 0;
+ position: absolute;
+ width: 100%;
+ }
+ th.label:after {
+ border-left: 1px solid #ddd;
+ top: 0;
}
.groupHeader {
- background-color: #f4f4f4;
+ background-color: #eaeaea;
}
.groupHeader a {
color: #000;
@@ -91,11 +111,30 @@
width: 30px;
}
.label {
+ border-left: 1px solid #ddd;
text-align: center;
width: 3rem;
}
- .label:not(.topHeader) {
- border-left: 1px solid #ddd;
+ .topHeader .label {
+ border: none;
+ }
+ .truncatedProject {
+ display: none;
+ }
+ @media only screen and (max-width: 90em) {
+ .assignee,
+ .branch,
+ .owner {
+ overflow: hidden;
+ max-width: 10rem;
+ text-overflow: ellipsis;
+ }
+ .truncatedProject {
+ display: inline-block;
+ }
+ .fullProject {
+ display: none;
+ }
}
@media only screen and (max-width: 50em) {
:host {
diff --git a/polygerrit-ui/app/template_test.sh b/polygerrit-ui/app/template_test.sh
index 5ffc434..a9710cd 100755
--- a/polygerrit-ui/app/template_test.sh
+++ b/polygerrit-ui/app/template_test.sh
@@ -22,6 +22,12 @@
exit 1
fi
+twinkie_version=$(npm list -g fried-twinkie@\>0.1 | grep fried-twinkie || :)
+if [ -z "$twinkie_version" ]; then
+ echo "Outdated version of fried-twinkie found. Bypassing template check."
+ exit 0
+fi
+
# Have to find where node_modules are installed and set the NODE_PATH
get_node_path() {
diff --git a/polygerrit-ui/app/template_test_srcs/template_test.js b/polygerrit-ui/app/template_test_srcs/template_test.js
index 7c64db3..3de6227 100644
--- a/polygerrit-ui/app/template_test_srcs/template_test.js
+++ b/polygerrit-ui/app/template_test_srcs/template_test.js
@@ -45,14 +45,18 @@
console.log('error /polygerrit-ui/temp/behaviors/ directory');
}
const behaviors = data;
- const externs = [];
+ const additionalSources = [];
+ const externMap = {};
for (const behavior of behaviors) {
- externs.push({
- path: `./polygerrit-ui/temp/behaviors/${behavior}`,
- src: fs.readFileSync(
- `./polygerrit-ui/temp/behaviors/${behavior}`, 'utf-8'),
- });
+ if (!externMap[behavior]) {
+ additionalSources.push({
+ path: `./polygerrit-ui/temp/behaviors/${behavior}`,
+ src: fs.readFileSync(
+ `./polygerrit-ui/temp/behaviors/${behavior}`, 'utf-8'),
+ });
+ externMap[behavior] = true;
+ }
}
let mappings = JSON.parse(fs.readFileSync(
@@ -83,28 +87,30 @@
mappings = mappingSpecificFile;
}
- externs.push({
+ additionalSources.push({
path: 'custom-externs.js',
src: '/** @externs */' +
EXTERN_NAMES.map( name => { return `var ${name};`; }).join(' '),
});
- const promises = [];
-
+ const toCheck = [];
for (key of Object.keys(mappings)) {
if (mappings[key].html && mappings[key].js) {
- promises.push(twinkie.checkTemplate(
- mappings[key].html,
- mappings[key].js,
- 'polygerrit.' + mappings[key].package,
- externs
- ));
+ toCheck.push({
+ htmlSrcPath: mappings[key].html,
+ jsSrcPath: mappings[key].js,
+ jsModule: 'polygerrit.' + mappings[key].package,
+ });
}
}
- Promise.all(promises).then(() => {}, joinedErrors => {
- if (joinedErrors) {
- process.exit(1);
- }
- });
+ twinkie.checkTemplate(toCheck, additionalSources)
+ .then(() => {}, joinedErrors => {
+ if (joinedErrors) {
+ process.exit(1);
+ }
+ }).catch(e => {
+ console.error(e);
+ process.exit(1);
+ });
});
diff --git a/polygerrit-ui/app/test/functional/README.md b/polygerrit-ui/app/test/functional/README.md
new file mode 100644
index 0000000..82c6133
--- /dev/null
+++ b/polygerrit-ui/app/test/functional/README.md
@@ -0,0 +1,54 @@
+# Functional test suite
+
+## Installing Docker (OSX)
+
+Simplest way to install all of those is to use Homebrew:
+
+```
+brew cask install docker
+```
+
+This will install a Docker in Applications. To run if from the command-line:
+
+```
+open /Applications/Docker.app
+```
+
+It'll require privileged access and will require user password to be entered.
+
+To validate Docker is installed correctly, run hello-world image:
+
+```
+docker run hello-world
+```
+
+## Building a Docker image
+
+Should be done once only for development purposes, run from the Gerrit checkout
+path:
+
+```
+docker build -t gerrit/polygerrit-functional:v1 \
+ polygerrit-ui/app/test/functional/infra
+```
+
+## Running a smoke test
+
+Running a smoke test from Gerrit checkout path:
+
+```
+./polygerrit-ui/app/test/functional/run_functional.sh
+```
+
+The successful output should be something similar to this:
+
+```
+Starting local server..
+Starting Webdriver..
+Started
+.
+
+
+1 spec, 0 failures
+Finished in 2.565 seconds
+```
diff --git a/polygerrit-ui/app/test/functional/infra/Dockerfile b/polygerrit-ui/app/test/functional/infra/Dockerfile
new file mode 100644
index 0000000..e642176
--- /dev/null
+++ b/polygerrit-ui/app/test/functional/infra/Dockerfile
@@ -0,0 +1,38 @@
+FROM selenium/standalone-chrome-debug
+
+USER root
+
+# nvm environment variables
+ENV NVM_DIR /usr/local/nvm
+ENV NODE_VERSION 9.4.0
+
+# install nvm
+# https://github.com/creationix/nvm#install-script
+RUN wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash
+
+# install node and npm
+RUN [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" \
+ && nvm install $NODE_VERSION \
+ && nvm alias default $NODE_VERSION \
+ && nvm use default
+
+ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules
+ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
+
+RUN npm install -g jasmine
+RUN npm install -g http-server
+
+USER seluser
+
+RUN mkdir -p /tmp/app
+WORKDIR /tmp/app
+
+RUN npm init -y
+RUN npm install --save selenium-webdriver
+
+EXPOSE 8080
+
+COPY test-infra.js /tmp/app/node_modules
+COPY run.sh /tmp/app/
+
+ENTRYPOINT [ "/tmp/app/run.sh" ]
diff --git a/polygerrit-ui/app/test/functional/infra/run.sh b/polygerrit-ui/app/test/functional/infra/run.sh
new file mode 100755
index 0000000..4beb3dd
--- /dev/null
+++ b/polygerrit-ui/app/test/functional/infra/run.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+echo Starting local server..
+cp /app/polygerrit_ui.zip .
+unzip -q polygerrit_ui.zip
+nohup http-server polygerrit_ui > /tmp/http-server.log 2>&1 &
+
+echo Starting Webdriver..
+nohup /opt/bin/entry_point.sh > /tmp/webdriver.log 2>&1 &
+
+# Wait for servers to start
+sleep 5
+
+cp $@ .
+jasmine $(basename $@)
diff --git a/polygerrit-ui/app/test/functional/infra/test-infra.js b/polygerrit-ui/app/test/functional/infra/test-infra.js
new file mode 100644
index 0000000..2619694
--- /dev/null
+++ b/polygerrit-ui/app/test/functional/infra/test-infra.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const {Builder} = require('selenium-webdriver');
+
+let driver;
+
+function setup() {
+ return new Builder()
+ .forBrowser('chrome')
+ .usingServer('http://localhost:4444/wd/hub')
+ .build()
+ .then(d => {
+ driver = d;
+ return driver.get('http://localhost:8080');
+ })
+ .then(() => driver);
+}
+
+function cleanup() {
+ return driver.quit();
+}
+
+exports.setup = setup;
+exports.cleanup = cleanup;
diff --git a/polygerrit-ui/app/test/functional/run_functional.sh b/polygerrit-ui/app/test/functional/run_functional.sh
new file mode 100755
index 0000000..7ce57b8
--- /dev/null
+++ b/polygerrit-ui/app/test/functional/run_functional.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+bazel build //polygerrit-ui/app:polygerrit_ui
+
+docker run --rm \
+ -p 5900:5900 \
+ -v `pwd`/polygerrit-ui/app/test/functional:/tests \
+ -v `pwd`/bazel-genfiles/polygerrit-ui/app:/app \
+ -it gerrit/polygerrit-functional:v1 \
+ /tests/test.js
diff --git a/polygerrit-ui/app/test/functional/test.js b/polygerrit-ui/app/test/functional/test.js
new file mode 100644
index 0000000..d394487
--- /dev/null
+++ b/polygerrit-ui/app/test/functional/test.js
@@ -0,0 +1,25 @@
+/**
+ * @fileoverview Minimal viable frontend functional test.
+ */
+'use strict';
+
+const {until} = require('selenium-webdriver');
+const {setup, cleanup} = require('test-infra');
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
+
+describe('example ', () => {
+ let driver;
+
+ beforeAll(() => {
+ return setup().then(d => driver = d);
+ });
+
+ afterAll(() => {
+ return cleanup();
+ });
+
+ it('should update title', () => {
+ return driver.wait(until.titleIs('status:open ยท Gerrit Code Review'), 5000);
+ });
+});
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index ced836c..2bb8b2e 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -55,6 +55,7 @@
'change-list/gr-change-list-item/gr-change-list-item_test.html',
'change-list/gr-change-list-view/gr-change-list-view_test.html',
'change-list/gr-change-list/gr-change-list_test.html',
+ 'change-list/gr-dashboard-view/gr-dashboard-view_test.html',
'change-list/gr-user-header/gr-user-header_test.html',
'change/gr-account-entry/gr-account-entry_test.html',
'change/gr-account-list/gr-account-list_test.html',
@@ -64,6 +65,7 @@
'change/gr-change-view/gr-change-view_test.html',
'change/gr-comment-list/gr-comment-list_test.html',
'change/gr-commit-info/gr-commit-info_test.html',
+ 'change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html',
'change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html',
'change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html',
'change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
@@ -79,6 +81,7 @@
'change/gr-reply-dialog/gr-reply-dialog-it_test.html',
'change/gr-reply-dialog/gr-reply-dialog_test.html',
'change/gr-reviewer-list/gr-reviewer-list_test.html',
+ 'change/gr-thread-list/gr-thread-list_test.html',
'core/gr-account-dropdown/gr-account-dropdown_test.html',
'core/gr-error-manager/gr-error-manager_test.html',
'core/gr-main-header/gr-main-header_test.html',
@@ -94,6 +97,7 @@
'diff/gr-diff-cursor/gr-diff-cursor_test.html',
'diff/gr-diff-highlight/gr-annotation_test.html',
'diff/gr-diff-highlight/gr-diff-highlight_test.html',
+ 'diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html',
'diff/gr-diff-preferences/gr-diff-preferences_test.html',
'diff/gr-diff-processor/gr-diff-processor_test.html',
'diff/gr-diff-selection/gr-diff-selection_test.html',
@@ -123,6 +127,7 @@
'settings/gr-cla-view/gr-cla-view_test.html',
'settings/gr-edit-preferences/gr-edit-preferences_test.html',
'settings/gr-email-editor/gr-email-editor_test.html',
+ 'settings/gr-gpg-editor/gr-gpg-editor_test.html',
'settings/gr-group-list/gr-group-list_test.html',
'settings/gr-http-password/gr-http-password_test.html',
'settings/gr-identities/gr-identities_test.html',
diff --git a/polygerrit-ui/edit-walkthrough/edit-walkthrough.md b/polygerrit-ui/edit-walkthrough/edit-walkthrough.md
new file mode 100644
index 0000000..717f683
--- /dev/null
+++ b/polygerrit-ui/edit-walkthrough/edit-walkthrough.md
@@ -0,0 +1,80 @@
+# In-browser Editing in Gerrit
+
+### What's going on?
+
+Until Q1 of 2018, editing a file in the browser was not supported by Gerrit's
+new UI. This feature is now done and ready for use.
+
+Read on for a walkthrough of the feature!
+
+### Creating an edit
+
+Click on the "Edit" button to begin.
+
+One may also go to the project mmanagement page (Browse => Repository =>
+Commands => Create Change) to create a new change.
+
+
+
+### Performing an action
+
+The buttons in the file list header open dialogs to perform actions on any file
+in the repo.
+
+* Open - opens an existing or new file from the repo in an editor.
+* Delete - deletes an existing file from the repo.
+* Rename - renames an existing file in the repo.
+
+To leave edit mode and restore the normal buttons to the file list, click "Stop
+editing".
+
+
+
+### Performing an action on a file
+
+The "Actions" dropdown appears on each file, and is used to perform actions on
+that specific file.
+
+* Open - opens this file in the editor.
+* Delete - deletes this file from the repo.
+* Rename - renames this file in the repo.
+* Restore - restores this file to the state it existed in at the patch the
+edit was created on.
+
+
+
+### Modifying the file
+
+This is the editor view.
+
+Clicking on the file path allows you to rename the file, You can edit code in
+the textarea, and "Close" will discard any unsaved changes and navigate back to
+the previous view.
+
+
+
+### Saving the edit
+
+You can save changes to the code with `cmd+s`, `ctrl+s`, or by clicking the
+"Save" button.
+
+
+
+### Publishing the edit
+
+You may publish or delete the edit by clicking the buttons in the header.
+
+
+
+
+
+### What if I have questions not answered here?
+
+Gerrit's [official docs](https://gerrit-review.googlesource.com/Documentation/user-inline-edit.html)
+are in the process of being updated and largely refer to the old UI, but the
+user experience is largely the same.
+
+Otherwise, please email
+[the repo-discuss mailing list](mailto:repo-discuss@google.com) or file a bug
+on Gerrit's official bug tracker,
+[Monorail](https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit+Issue).
\ No newline at end of file
diff --git a/polygerrit-ui/edit-walkthrough/img/actions_overflow.png b/polygerrit-ui/edit-walkthrough/img/actions_overflow.png
new file mode 100644
index 0000000..bf39763
--- /dev/null
+++ b/polygerrit-ui/edit-walkthrough/img/actions_overflow.png
Binary files differ
diff --git a/polygerrit-ui/edit-walkthrough/img/edit_made.png b/polygerrit-ui/edit-walkthrough/img/edit_made.png
new file mode 100644
index 0000000..658245d
--- /dev/null
+++ b/polygerrit-ui/edit-walkthrough/img/edit_made.png
Binary files differ
diff --git a/polygerrit-ui/edit-walkthrough/img/edit_pending.png b/polygerrit-ui/edit-walkthrough/img/edit_pending.png
new file mode 100644
index 0000000..a63f6ee
--- /dev/null
+++ b/polygerrit-ui/edit-walkthrough/img/edit_pending.png
Binary files differ
diff --git a/polygerrit-ui/edit-walkthrough/img/in_edit_mode.png b/polygerrit-ui/edit-walkthrough/img/in_edit_mode.png
new file mode 100644
index 0000000..582ed66
--- /dev/null
+++ b/polygerrit-ui/edit-walkthrough/img/in_edit_mode.png
Binary files differ
diff --git a/polygerrit-ui/edit-walkthrough/img/in_editor.png b/polygerrit-ui/edit-walkthrough/img/in_editor.png
new file mode 100644
index 0000000..228d020
--- /dev/null
+++ b/polygerrit-ui/edit-walkthrough/img/in_editor.png
Binary files differ
diff --git a/polygerrit-ui/edit-walkthrough/img/into_edit.png b/polygerrit-ui/edit-walkthrough/img/into_edit.png
new file mode 100644
index 0000000..b6c14ed
--- /dev/null
+++ b/polygerrit-ui/edit-walkthrough/img/into_edit.png
Binary files differ
diff --git a/resources/com/google/gerrit/pgm/ProtoGenHeader.txt b/resources/com/google/gerrit/pgm/ProtoGenHeader.txt
index 757e8e2..a380955 100644
--- a/resources/com/google/gerrit/pgm/ProtoGenHeader.txt
+++ b/resources/com/google/gerrit/pgm/ProtoGenHeader.txt
@@ -11,8 +11,6 @@
// 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.
-//
-// Gerrit Code Review (version @@VERSION@@)
syntax = "proto2";
diff --git a/resources/com/google/gerrit/server/mail/AddKey.soy b/resources/com/google/gerrit/server/mail/AddKey.soy
index af99569..be76aee 100644
--- a/resources/com/google/gerrit/server/mail/AddKey.soy
+++ b/resources/com/google/gerrit/server/mail/AddKey.soy
@@ -64,8 +64,5 @@
browser window instead.
{\n}
- {\n}
-
- This is a send-only email address. Replies to this message will not be read
- or answered.
+ {call .NoReplyFooter /}
{/template}
diff --git a/resources/com/google/gerrit/server/mail/AddKeyHtml.soy b/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
index 712abc7..04a0635 100644
--- a/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
+++ b/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
@@ -59,8 +59,5 @@
{/if}.
</p>
- <p>
- This is a send-only email address. Replies to this message will not be read
- or answered.
- </p>
+ {call .NoReplyFooterHtml /}
{/template}
diff --git a/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy b/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy
new file mode 100644
index 0000000..e997776
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/InboundEmailRejection.soy
@@ -0,0 +1,64 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+{template .InboundEmailRejectionFooter kind="text"}
+ {\n}
+ {\n}
+ Thus, no actions were taken by Gerrit in response to this email,
+ and you should use the Gerrit website to continue.
+ {\n}
+ This email was sent in response to an email coming from this address.
+ In case you did not send Gerrit an email, feel free to ignore this.
+ {call .NoReplyFooter /}
+{/template}
+
+/**
+ * The .InboundEmailRejection templates will determine the contents of the email related
+ * to warning users of error in inbound emails
+ */
+
+{template .InboundEmailRejection_PARSING_ERROR kind="text"}
+ Gerrit Code Review was unable to parse your email.{\n}
+ This might be because your email did not quote Gerrit's email,
+ because you are using an unsupported email client,
+ or because of a bug.
+ {call .InboundEmailRejectionFooter /}
+{/template}
+
+{template .InboundEmailRejection_UNKNOWN_ACCOUNT kind="text"}
+ Gerrit Code Review was unable to match your email to an account.{\n}
+ This may happen if several accounts are linked to this email address.
+ {call .InboundEmailRejectionFooter /}
+{/template}
+
+{template .InboundEmailRejection_INACTIVE_ACCOUNT kind="text"}
+ Your account on this Gerrit Code Review instance is marked as inactive,
+ so your email has been ignored. {\n}
+ If you think this is an error, please contact your Gerrit instance administrator.
+ {\n}{\n}
+ This email was sent in response to an email coming from this address.
+ In case you did not send Gerrit an email, feel free to ignore this.
+ {call .NoReplyFooter /}
+{/template}
+
+{template .InboundEmailRejection_INTERNAL_EXCEPTION kind="text"}
+ Gerrit Code Review encountered an internal exception and was unable to fulfil your request.
+ {\n}
+ This might be caused by an ongoing maintenance or a data corruption.
+ {call .InboundEmailRejectionFooter /}
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy b/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy
new file mode 100644
index 0000000..f879270
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/InboundEmailRejectionHtml.soy
@@ -0,0 +1,80 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+
+{template .InboundEmailRejectionFooterHtml}
+ <p>
+ Thus, no actions were taken by Gerrit in response to this email,
+ and you should use the Gerrit website to continue.
+ </p>
+ <p>
+ In case you did not send Gerrit an email, feel free to ignore this.
+ </p>
+ {call .NoReplyFooterHtml /}
+{/template}
+
+/**
+ * The .InboundEmailRejection templates will determine the contents of the email related
+ * to warning users of error in inbound emails
+ */
+
+{template .InboundEmailRejectionHtml_PARSING_ERROR}
+ <p>
+ Gerrit Code Review was unable to parse your email.
+ </p>
+ <p>
+ This might be because your email did not quote Gerrit's email,
+ because you are using an unsupported email client,
+ or because of a bug.
+ </p>
+ {call .InboundEmailRejectionFooterHtml /}
+{/template}
+
+{template .InboundEmailRejectionHtml_UNKNOWN_ACCOUNT}
+ <p>
+ Gerrit Code Review was unable to match your email to an account.
+ </p>
+ <p>
+ This may happen if several accounts are linked to this email address.
+ </p>
+ {call .InboundEmailRejectionFooterHtml /}
+{/template}
+
+{template .InboundEmailRejectionHtml_INACTIVE_ACCOUNT}
+ <p>
+ Your account on this Gerrit Code Review instance is marked as inactive,
+ so your email has been ignored.
+ </p>
+ <p>
+ If you think this is an error, please contact your Gerrit instance administrator.
+ </p>
+ <p>
+ In case you did not send Gerrit an email, feel free to ignore this.
+ </p>
+ {call .NoReplyFooter /}
+{/template}
+
+{template .InboundEmailRejectionHtml_INTERNAL_EXCEPTION}
+ <p>
+ Gerrit Code Review encountered an internal exception and was unable to fulfil your request.
+ </p>
+ <p>
+ This might be caused by an ongoing maintenance or a data corruption.
+ <p>
+ {call .InboundEmailRejectionFooterHtml /}
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/Merged.soy b/resources/com/google/gerrit/server/mail/Merged.soy
index 1d6ae9c..40924e6 100644
--- a/resources/com/google/gerrit/server/mail/Merged.soy
+++ b/resources/com/google/gerrit/server/mail/Merged.soy
@@ -1,3 +1,4 @@
+
/**
* Copyright (C) 2016 The Android Open Source Project
*
@@ -21,16 +22,10 @@
* a change successfully merged to the head.
* @param change
* @param email
- * @param fromEmail
* @param fromName
- * @param patchSetInfo
*/
{template .Merged kind="text"}
- {$fromName} merged this change
- {if $patchSetInfo.authorEmail != $fromEmail}
- {sp}by {$patchSetInfo.authorName}
- {/if}.
-
+ {$fromName} has submitted this change and it was merged.
{if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n}
{\n}
Change subject: {$change.subject}{\n}
diff --git a/resources/com/google/gerrit/server/mail/MergedHtml.soy b/resources/com/google/gerrit/server/mail/MergedHtml.soy
index 414479e..b11c5e5 100644
--- a/resources/com/google/gerrit/server/mail/MergedHtml.soy
+++ b/resources/com/google/gerrit/server/mail/MergedHtml.soy
@@ -19,16 +19,11 @@
/**
* @param diffLines
* @param email
- * @param fromEmail
* @param fromName
- * @param patchSetInfo
*/
{template .MergedHtml}
<p>
- {$fromName} <strong>merged</strong> this change
- {if $patchSetInfo.authorEmail != $fromEmail}
- {sp}by {$patchSetInfo.authorName}
- {/if}.
+ {$fromName} <strong>merged</strong> this change.
</p>
{if $email.changeUrl}
diff --git a/resources/com/google/gerrit/server/mail/NoReplyFooter.soy b/resources/com/google/gerrit/server/mail/NoReplyFooter.soy
new file mode 100644
index 0000000..1443100
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/NoReplyFooter.soy
@@ -0,0 +1,23 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+{template .NoReplyFooter kind="text"}
+ {\n}
+ This is a send-only email address. Replies to this message will not be read
+ or answered.
+{/template}
diff --git a/resources/com/google/gerrit/server/mail/NoReplyFooterHtml.soy b/resources/com/google/gerrit/server/mail/NoReplyFooterHtml.soy
new file mode 100644
index 0000000..93df527
--- /dev/null
+++ b/resources/com/google/gerrit/server/mail/NoReplyFooterHtml.soy
@@ -0,0 +1,24 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+{template .NoReplyFooterHtml}
+ <p>
+ This is a send-only email address. Replies to this message will not be read
+ or answered.
+ </p>
+{/template}
diff --git a/tools/bzl/gwt.bzl b/tools/bzl/gwt.bzl
index a733b0c..b09a608 100644
--- a/tools/bzl/gwt.bzl
+++ b/tools/bzl/gwt.bzl
@@ -12,8 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-# Port of Buck native gwt_binary() rule. See discussion in context of
-# https://github.com/facebook/buck/issues/109
load("//tools/bzl:genrule2.bzl", "genrule2")
load("//tools/bzl:java.bzl", "java_library2")
diff --git a/tools/bzl/license-map.py b/tools/bzl/license-map.py
index 1c8db72..79aed37 100644
--- a/tools/bzl/license-map.py
+++ b/tools/bzl/license-map.py
@@ -6,8 +6,8 @@
import argparse
from collections import defaultdict
-from shutil import copyfileobj
from sys import stdout, stderr
+import os
import xml.etree.ElementTree as ET
@@ -113,8 +113,8 @@
print()
print("[[%s_license]]" % safename)
print("----")
- with open(n[2:].replace(":", "/")) as fd:
- copyfileobj(fd, stdout)
+ with open(n[2:].replace(":", "/"), "rb") as input:
+ os.write(stdout.fileno(), input.read(-1))
print()
print("----")
print()
diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl
index 3578173..38dfbe5 100644
--- a/tools/bzl/license.bzl
+++ b/tools/bzl/license.bzl
@@ -25,7 +25,7 @@
# post process the XML into our favorite format.
native.genrule(
name = "gen_license_txt_" + name,
- cmd = "python2 $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
+ cmd = "python $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
outs = [ name + ".txt" ],
tools = tools,
**kwargs
diff --git a/tools/download_file.py b/tools/download_file.py
index 1a32bbf..26671f0 100755
--- a/tools/download_file.py
+++ b/tools/download_file.py
@@ -25,8 +25,7 @@
from zipfile import ZipFile, BadZipfile, LargeZipFile
GERRIT_HOME = path.expanduser('~/.gerritcodereview')
-# TODO(davido): Rename in bazel-cache
-CACHE_DIR = path.join(GERRIT_HOME, 'buck-cache', 'downloaded-artifacts')
+CACHE_DIR = path.join(GERRIT_HOME, 'bazel-cache', 'downloaded-artifacts')
LOCAL_PROPERTIES = 'local.properties'
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py
index 9f8b4b7..9374d88 100755
--- a/tools/eclipse/project.py
+++ b/tools/eclipse/project.py
@@ -13,7 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-# TODO(sop): Remove hack after Buck supports Eclipse
from __future__ import print_function
# TODO(davido): use Google style for importing instead:
diff --git a/tools/js/bower2bazel.py b/tools/js/bower2bazel.py
index 7479021..ccdf2df 100755
--- a/tools/js/bower2bazel.py
+++ b/tools/js/bower2bazel.py
@@ -58,11 +58,13 @@
"neon-animation": "polymer",
"page": "page.js",
"paper-button": "polymer",
+ "paper-icon-button": "polymer",
"paper-input": "polymer",
"paper-item": "polymer",
"paper-listbox": "polymer",
"paper-toggle-button": "polymer",
"paper-styles": "polymer",
+ "paper-tabs": "polymer",
"polymer": "polymer",
"polymer-resin": "polymer",
"promise-polyfill": "promise-polyfill",
diff --git a/tools/js/download_bower.py b/tools/js/download_bower.py
index 1c8d45e..3db39d5 100755
--- a/tools/js/download_bower.py
+++ b/tools/js/download_bower.py
@@ -26,7 +26,7 @@
import bowerutil
CACHE_DIR = os.path.expanduser(os.path.join(
- '~', '.gerritcodereview', 'buck-cache', 'downloaded-artifacts'))
+ '~', '.gerritcodereview', 'bazel-cache', 'downloaded-artifacts'))
def bower_cmd(bower, *args):
diff --git a/tools/maven/mvn.py b/tools/maven/mvn.py
index 2e1c1a9..50c4ac6 100755
--- a/tools/maven/mvn.py
+++ b/tools/maven/mvn.py
@@ -16,7 +16,7 @@
from __future__ import print_function
from optparse import OptionParser
from os import path, environ
-from subprocess import check_output
+from subprocess import check_output, CalledProcessError
from sys import stderr
opts = OptionParser()
@@ -67,6 +67,8 @@
except Exception as e:
print('%s command failed: %s\n%s' % (args.a, ' '.join(exe), e),
file=stderr)
+ if environ.get('VERBOSE') and isinstance(e, CalledProcessError):
+ print('Command output\n%s' % e.output, file=stderr)
exit(1)