Merge "Introduce sequential success commit messages"
diff --git a/.mailmap b/.mailmap
index cbf1f3b..c863847 100644
--- a/.mailmap
+++ b/.mailmap
@@ -12,6 +12,7 @@
Carlos Eduardo Baldacin <carloseduardo.baldacin@sonyericsson.com> carloseduardo.baldacin <carloseduardo.baldacin@sonyericsson.com>
Changcheng Xiao <xchangcheng@google.com> xchangcheng
Dariusz Luksza <dluksza@collab.net> <dariusz@luksza.org>
+Darrien Glasser <darrien@arista.com> darrien <darrien@arista.com>
Dave Borowitz <dborowitz@google.com> <dborowitz@google.com>
David Ostrovsky <david@ostrovsky.org> <d.ostrovsky@gmx.de>
David Ostrovsky <david@ostrovsky.org> <david.ostrovsky@gmail.com>
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index c514321..2e7cf17 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -1358,7 +1358,8 @@
any of their groups is used.
This limit applies not only to the link:cmd-query.html[`gerrit query`]
-command, but also to the web UI results pagination size.
+command, but also to the web UI results pagination size in the new
+PolyGerrit UI and, limited to the full project list, in the old GWT UI.
[[capability_readAs]]
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index 3bb8e4f..fb35dc2 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -14,6 +14,7 @@
[--format {text | json | json_compact}]
[--all]
[--limit <N>]
+ [--prefix | -p <prefix>]
[--has-acl-for GROUP]
--
@@ -87,6 +88,9 @@
--limit::
Cap the number of results to the first N matches.
+--prefix::
+ Limit the results to those projects that start with the specified prefix.
+
--has-acl-for::
Display only projects on which access rights for this group are
directly assigned. Projects which only inherit access rights for
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 71385e2..b15aea7 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -21,7 +21,7 @@
[--verified <N>] [--code-review <N>]
[--label Label-Name=<N>]
[--tag TAG]
- {COMMIT | CHANGEID,PATCHSET}...
+ {COMMIT | CHANGENUMBER,PATCHSET}...
--
== DESCRIPTION
@@ -144,19 +144,24 @@
Approve the change with commit c0ff33 as "Verified +1"
----
-$ ssh -p 29418 review.example.com gerrit review --verified +1 c0ff33
+$ ssh -p 29418 review.example.com gerrit review --verified +1 8242,2
+----
+
+Approve the change with change number 8242 and patch set 2 as "Code-Review +2"
+----
+$ ssh -p 29418 review.example.com gerrit review --code-review +2 8242,2
----
Vote on the project specific label "mylabel":
----
-$ ssh -p 29418 review.example.com gerrit review --label mylabel=+1 c0ff33
+$ ssh -p 29418 review.example.com gerrit review --label mylabel=+1 8242,2
----
Append the message "Build Successful". Notice two levels of quoting is
required, one for the local shell, and another for the argument parser
inside the Gerrit server:
----
-$ ssh -p 29418 review.example.com gerrit review -m '"Build Successful"' c0ff33
+$ ssh -p 29418 review.example.com gerrit review -m '"Build Successful"' 8242,2
----
Mark the unmerged commits both "Verified +1" and "Code-Review +2" and
@@ -172,7 +177,7 @@
Abandon an active change:
----
-$ ssh -p 29418 review.example.com gerrit review --abandon c0ff33
+$ ssh -p 29418 review.example.com gerrit review --abandon 8242,2
----
== SEE ALSO
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index f5a226d..aad733a 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -32,7 +32,7 @@
=== Section accountPatchReviewDb
The AccountPatchReviewDb is a database used to store the user file reviewed
-flags. It co-exists with <<database,ReviewDb>> and link:note-db.html[NoteDb].
+flags.
[[accountPatchReviewDb.url]]accountPatchReviewDb.url::
+
@@ -2844,6 +2844,7 @@
* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.3/security-getting-started.html[Elasticsearch 6.3]
* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.4/security-getting-started.html[Elasticsearch 6.4]
* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.5/security-getting-started.html[Elasticsearch 6.5]
+* link:https://www.elastic.co/guide/en/elastic-stack-overview/6.6/security-getting-started.html[Elasticsearch 6.6]
[[elasticsearch.username]]elasticsearch.username::
+
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index efa17da..bec1984 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -286,33 +286,6 @@
bazel test //javatests/com/google/gerrit/acceptance/rest/account:rest_account
----
-The tests run with NoteDb fully enabled by default.
-
-To run the tests against NoteDb backend with write to NoteDb, but not read from
-it:
-
-----
- bazel test --test_env=GERRIT_NOTEDB=WRITE //...
-----
-
-Write and read from NoteDb:
-
-----
- bazel test --test_env=GERRIT_NOTEDB=READ_WRITE //...
-----
-
-Primary storage NoteDb:
-
-----
- bazel test --test_env=GERRIT_NOTEDB=PRIMARY //...
-----
-
-NoteDb entirely disabled:
-
-----
- bazel test --test_env=GERRIT_NOTEDB=OFF //...
-----
-
To run only tests that do not use SSH:
----
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 3b1c501..366e216 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -166,7 +166,7 @@
link:https://github.com/google/google-java-format[`google-java-format`]
tool (version 1.6), and to format Bazel BUILD, WORKSPACE and .bzl files the
link:https://github.com/bazelbuild/buildtools/tree/master/buildifier[`buildifier`]
-tool (version 0.17.2).
+tool (version 0.20.0).
These tools automatically apply format according to the style guides; this
streamlines code review by reducing the need for time-consuming, tedious,
and contentious discussions about trivial issues like whitespace.
diff --git a/Documentation/dev-intellij.txt b/Documentation/dev-intellij.txt
index ea51977..ca47690 100644
--- a/Documentation/dev-intellij.txt
+++ b/Documentation/dev-intellij.txt
@@ -184,10 +184,6 @@
plugin manages a project, it intercepts the creation and creates a Bazel test
run configuration instead, which can be used just like the standard ones.
-TIP: Tests run with NoteDb enabled by default. If you would like to execute a
-test with NoteDb turned off, add `--test_env=GERRIT_NOTEDB=OFF` to the *Bazel
-flags* of your run configuration.
-
[[remote-debug]]
=== Debugging a remote Gerrit server
If a remote Gerrit server is running and has opened a debug port, you can attach
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index de5f278..c326b66 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -2141,12 +2141,37 @@
This can be:
+* `self` or `me` for the calling user
+* a bare account ID ("18419")
+* an account ID following a name in parentheses ("Full Name (18419)")
* a string of the format "Full Name <email@example.com>"
* just the email address ("email@example")
-* a full name if it is unique ("Full Name")
-* an account ID ("18419")
+* a full name ("Full Name")
* a user name ("username")
-* `self` for the calling user
+
+In all cases, accounts that are not
+link:config-gerrit.txt#accounts.visibility[visible] to the calling user are not
+considered.
+
+In all cases _except_ a bare account ID and `self`/`me`, inactive accounts are
+not considered. Inactive accounts should only be referenced by bare ID.
+
+If the input is a bare account ID, this will always resolve to exactly
+one account if there is a visible account with that ID, and zero accounts
+otherwise. (This is true even in corner cases like a user having a full name
+which is exactly a numeric account ID belonging to a different user; such a user
+cannot be identified by this number.)
+
+If the identifier is ambiguous or only refers to inactive accounts, the error
+message from the API should contain a human-readable description of how to
+disambiguate the request.
+
+*Note*: Except as noted above, callers should not rely on the particular
+priorities of any of the identifiers in the account resolution algorithm. Any
+other formats may be subject to future deprecation. If callers require specific
+searching semantics, they should use the link:#query-account[Query Account]
+endpoint to resolve a string to one or more accounts, then access the API using
+the account ID.
[[capability-id]]
=== \{capability-id\}
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 325fbeb..01b2545 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -310,9 +310,13 @@
link:#change-info[ChangeInfo]. For fast moving projects, this field must
be recomputed often, which is slow for projects with big trees.
+
-This option is deprecated. In a future release, `mergeable` will not be
-populated in link:#change-info[ChangeInfo]. It can be requested separately
-by calling the link:#get-mergeable[get-mergeable] endpoint.
+When link:config-gerrit.html#change.api.excludeMergeableInChangeInfo[
+`change.api.excludeMergeableInChangeInfo`] is set in the `gerrit.config`,
+the `mergeable` field will always be omitted and `SKIP_MERGEABLE` has no
+effect.
++
+A change's mergeability can be requested separately by calling the
+link:#get-mergeable[get-mergeable] endpoint.
--
[[submittable]]
diff --git a/Documentation/user-search-projects.txt b/Documentation/user-search-projects.txt
index 11c1326..8ebbf3e 100644
--- a/Documentation/user-search-projects.txt
+++ b/Documentation/user-search-projects.txt
@@ -12,6 +12,11 @@
+
Matches projects that have exactly the name 'NAME'.
+[[parent]]
+parent:'PARENT'::
++
+Matches projects that have 'PARENT' as parent project.
+
[[inname]]
inname:'NAME'::
+
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index abd2531..5d7a78b 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -276,6 +276,8 @@
ones using a bracket expression). For example, to match all XML
files named like 'name1.xml', 'name2.xml', and 'name3.xml' use
`file:"^name[1-3].xml"`.
++
+Slash ('/') is used path separator.
[[file]]
file:'NAME', f:'NAME'::
@@ -294,8 +296,40 @@
+
Matches any change touching a file with extension 'EXT', case-insensitive. The
extension is defined as the portion of the filename following the final `.`.
-Files with no `.` in their name have no extension and cannot be matched with
-this operator; use `file:` instead.
+Files with no `.` in their name have no extension and can be matched by an
+empty string.
+
+[[onlyextensions]]
+onlyextensions:'EXT_LIST', onlyexts:'EXT_LIST'::
++
+Matches any change touching only files with extensions that are listed in
+'EXT_LIST' (comma-separated list). The matching is done case-insensitive.
+An extension is defined as the portion of the filename following the final `.`.
+Files with no `.` in their name have no extension and can be matched by an
+empty string.
+
+[[directory]]
+directory:'DIR', dir:'DIR'::
++
+Matches any change where the current patch set touches a file in the directory
+'DIR'. The matching is done case-insensitive. 'DIR' can be a full directory
+name, a directory prefix or any combination of intermediate directory segments.
+E.g. a change that touches a file in the directory 'a/b/c' matches for 'a/b/c',
+'a', 'a/b', 'b', 'b/c' and 'c'.
++
+Slash ('/') is used path separator. Leading and trailing slashes are allowed
+but are not mandatory.
++
+If 'DIR' starts with `^` it matches directories and directory segments by
+regular expression. The link:http://www.brics.dk/automaton/[dk.brics.automaton
+library] is used for evaluation of such patterns.
+
+[[footer]]
+footer:'FOOTER'::
++
+Matches any change that has 'FOOTER' as footer in the commit message of the
+current patch set. 'FOOTER' can be specified verbatim ('<key>: <value>', must
+be quoted) or as '<key>=<value>'. The matching is done case-insensitive.
[[star]]
star:'LABEL'::
diff --git a/WORKSPACE b/WORKSPACE
index 0da1b3e..189923b 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -31,7 +31,7 @@
load("@bazel_skylib//lib:versions.bzl", "versions")
-versions.check(minimum_bazel_version = "0.19.0")
+versions.check(minimum_bazel_version = "0.22.0")
load("@io_bazel_rules_closure//closure:defs.bzl", "closure_repositories")
@@ -961,60 +961,60 @@
sha1 = "75070c744a8e52a7d17b8b476468580309d5cd09",
)
-JETTY_VERS = "9.4.12.v20180830"
+JETTY_VERS = "9.4.14.v20181114"
maven_jar(
name = "jetty-servlet",
artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS,
- sha1 = "4c1149328eda9fa39a274262042420f66d9ffd5f",
+ sha1 = "96f501462af425190ff7b63e387692c1aa3af2c8",
)
maven_jar(
name = "jetty-security",
artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS,
- sha1 = "299e0602a9c0b753ba232cc1c1dda72ddd9addcf",
+ sha1 = "6cbeb2fe9b3cc4f88a7ea040b8a0c4f703cd72ce",
)
maven_jar(
name = "jetty-servlets",
artifact = "org.eclipse.jetty:jetty-servlets:" + JETTY_VERS,
- sha1 = "53745200718fe4ddf57f04ad3ba34778a6aca585",
+ sha1 = "38cfc07b53e5d285bb2fca78bb2531565ed9c9e5",
)
maven_jar(
name = "jetty-server",
artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS,
- sha1 = "b0f25df0d32a445fd07d5f16fff1411c16b888fa",
+ sha1 = "b36a3d52d78a1df6406f6fa236a6eeff48cbfef6",
)
maven_jar(
name = "jetty-jmx",
artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS,
- sha1 = "7e9e589dd749a8c096008c0c4af863a81e67c55b",
+ sha1 = "3e02463d2bff175a3231cd3dc26363eaf76a3b17",
)
maven_jar(
name = "jetty-continuation",
artifact = "org.eclipse.jetty:jetty-continuation:" + JETTY_VERS,
- sha1 = "5f6d6e06f95088a3a7118b9065bc49ce7c014b75",
+ sha1 = "ac4981a61bcaf4e2538de6270300a870224a16b8",
)
maven_jar(
name = "jetty-http",
artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS,
- sha1 = "1341796dde4e16df69bca83f3e87688ba2e7d703",
+ sha1 = "6d0c8ac42e9894ae7b5032438eb4579c2a47f4fe",
)
maven_jar(
name = "jetty-io",
artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS,
- sha1 = "e93f5adaa35a9a6a85ba130f589c5305c6ecc9e3",
+ sha1 = "a8c6a705ddb9f83a75777d89b0be59fcef3f7637",
)
maven_jar(
name = "jetty-util",
artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS,
- sha1 = "cb4ccec9bd1fe4b10a04a0fb25d7053c1050188a",
+ sha1 = "5bb3d7a38f7ea54138336591d89dd5867b806c02",
)
maven_jar(
@@ -1058,8 +1058,8 @@
# and httpasyncclient as necessary.
maven_jar(
name = "elasticsearch-rest-client",
- artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.5.4",
- sha1 = "552175b06e34df96f114d1c8aaa908e535c8f1be",
+ artifact = "org.elasticsearch.client:elasticsearch-rest-client:6.6.0",
+ sha1 = "f0ce1ea819fedde731511b440b025e4fb5a2f5f7",
)
JACKSON_VERSION = "2.9.8"
@@ -1110,22 +1110,22 @@
maven_jar(
name = "mockito",
- artifact = "org.mockito:mockito-core:2.23.4",
- sha1 = "a35b6f8ffcfa786771eac7d7d903429e790fdf3f",
+ artifact = "org.mockito:mockito-core:2.24.0",
+ sha1 = "969a7bcb6f16e076904336ebc7ca171d412cc1f9",
)
-BYTE_BUDDY_VERSION = "1.9.3"
+BYTE_BUDDY_VERSION = "1.9.7"
maven_jar(
name = "byte-buddy",
artifact = "net.bytebuddy:byte-buddy:" + BYTE_BUDDY_VERSION,
- sha1 = "f32e510b239620852fc9a2387fac41fd053d6a4d",
+ sha1 = "8fea78fea6449e1738b675cb155ce8422661e237",
)
maven_jar(
name = "byte-buddy-agent",
artifact = "net.bytebuddy:byte-buddy-agent:" + BYTE_BUDDY_VERSION,
- sha1 = "f5b78c16cf4060664d80b6ca32d80dca4bd3d264",
+ sha1 = "8e7d1b599f4943851ffea125fd9780e572727fc0",
)
maven_jar(
@@ -1296,10 +1296,17 @@
)
bower_archive(
+ name = "resemblejs",
+ package = "rsmbl/Resemble.js",
+ sha1 = "49d5f022417c389b630d6f7ee667aa9540075c42",
+ version = "2.10.1",
+)
+
+bower_archive(
name = "codemirror-minified",
package = "Dominator008/codemirror-minified",
- sha1 = "1524e19087d8223edfe4a5b1ccf04c1e3707235d",
- version = "5.37.0",
+ sha1 = "e6bda82afc7cf3493f4282c6f17265d40e1485e5",
+ version = "5.43.0",
)
# bower test stuff
diff --git a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index 1712746..e6e0259 100644
--- a/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -105,14 +105,12 @@
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.group.db.Groups;
import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gerrit.server.index.account.AccountIndexer;
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.notedb.AbstractChangeNotes;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -282,8 +280,6 @@
@Inject private AccountIndexer accountIndexer;
@Inject private ChangeIndexCollection changeIndexes;
@Inject private EventRecorder.Factory eventRecorderFactory;
- @Inject private GroupIndexer groupIndexer;
- @Inject private Groups groups;
@Inject private InProcessProtocol inProcessProtocol;
@Inject private ProjectIndexCollection projectIndexes;
@Inject private ProjectOperations projectOperations;
@@ -370,13 +366,6 @@
accountIndexer.index(accountId);
}
- private void reindexAllGroups() throws IOException, ConfigInvalidException {
- Iterable<GroupReference> allGroups = groups.getAllGroupReferences()::iterator;
- for (GroupReference group : allGroups) {
- groupIndexer.index(group.getUUID());
- }
- }
-
protected static Config submitWholeTopicEnabledConfig() {
Config cfg = new Config();
cfg.setBoolean("change", null, "submitWholeTopic", true);
@@ -420,18 +409,6 @@
Transport.register(inProcessProtocol);
toClose = Collections.synchronizedList(new ArrayList<>());
- // All groups which were added during the server start (e.g. in SchemaCreatorImpl) aren't
- // contained in the instance of the group index which is available here and in tests. There are
- // two reasons:
- // 1) No group index is available in SchemaCreatorImpl when using an in-memory database.
- // (This could be fixed by using the IndexManagerOnInit in InMemoryTestingDatabaseModule similar
- // to how BaseInit uses it.)
- // 2) During the on-init part of the server start, we use another instance of the index than
- // 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.
- reindexAllGroups();
-
admin = accountCreator.admin();
user = accountCreator.user();
diff --git a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
index 4877f05..33b3e91 100644
--- a/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
+++ b/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -103,9 +103,10 @@
super(failureMetadata, target);
}
- public FakeEmailSenderSubject notSent() {
- if (actual().peekMessage() != null) {
- failWithoutActual(fact("expected message", "sent"));
+ public FakeEmailSenderSubject didNotSend() {
+ Message message = actual().peekMessage();
+ if (message != null) {
+ failWithoutActual(fact("expected no message", message));
}
return this;
}
@@ -133,7 +134,13 @@
}
EmailHeader header = message.headers().get("X-Gerrit-MessageType");
if (!header.equals(new EmailHeader.String(messageType))) {
- failWithoutActual(fact("expected message of type", messageType));
+ failWithoutActual(
+ fact("expected message of type", messageType),
+ fact(
+ "actual",
+ header instanceof EmailHeader.String
+ ? ((EmailHeader.String) header).getString()
+ : header));
}
// Return a named subject that displays a human-readable table of
@@ -493,17 +500,22 @@
}
protected StagedChange stageReviewableChange() throws Exception {
- return new StagedChange("refs/for/master");
+ StagedChange sc = new StagedChange("refs/for/master");
+ sender.clear();
+ return sc;
}
protected StagedChange stageWipChange() throws Exception {
- return new StagedChange("refs/for/master%wip");
+ StagedChange sc = new StagedChange("refs/for/master%wip");
+ sender.clear();
+ return sc;
}
protected StagedChange stageReviewableWipChange() throws Exception {
StagedChange sc = stageReviewableChange();
requestScopeOperations.setApiUser(sc.owner.getId());
gApi.changes().id(sc.changeId).setWorkInProgress();
+ sender.clear();
return sc;
}
diff --git a/java/com/google/gerrit/acceptance/GerritServer.java b/java/com/google/gerrit/acceptance/GerritServer.java
index 570b59c..7571184 100644
--- a/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/java/com/google/gerrit/acceptance/GerritServer.java
@@ -378,7 +378,9 @@
site);
daemon.setEmailModuleForTesting(new FakeEmailSender.Module());
daemon.setAuditEventModuleForTesting(new FakeGroupAuditService.Module());
- daemon.setAdditionalSysModuleForTesting(testSysModule);
+ if (testSysModule != null) {
+ daemon.addAdditionalSysModuleForTesting(testSysModule);
+ }
daemon.setEnableSshd(desc.useSsh());
if (desc.memory()) {
@@ -419,6 +421,8 @@
bind(GerritRuntime.class).toInstance(GerritRuntime.DAEMON);
}
}));
+ daemon.addAdditionalSysModuleForTesting(
+ new ReindexProjectsAtStartup.Module(), new ReindexGroupsAtStartup.Module());
daemon.start();
return new GerritServer(desc, null, createTestInjector(daemon), daemon, null);
}
diff --git a/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java b/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
new file mode 100644
index 0000000..84e798c
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/ReindexGroupsAtStartup.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.index.group.GroupIndexer;
+import com.google.inject.Inject;
+import com.google.inject.Scopes;
+import java.io.IOException;
+import java.util.stream.Stream;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+/** Reindex all groups at Gerrit daemon startup. */
+public class ReindexGroupsAtStartup implements LifecycleListener {
+ private final GroupIndexer groupIndexer;
+ private final Groups groups;
+ private final Config cfg;
+
+ public static class Module extends LifecycleModule {
+ @Override
+ protected void configure() {
+ listener().to(ReindexGroupsAtStartup.class).in(Scopes.SINGLETON);
+ }
+ }
+
+ @Inject
+ public ReindexGroupsAtStartup(
+ GroupIndexer groupIndexer, Groups groups, @GerritServerConfig Config cfg) {
+ this.groupIndexer = groupIndexer;
+ this.groups = groups;
+ this.cfg = cfg;
+ }
+
+ @Override
+ public void start() {
+ // Gerrit slaves without a reindex
+ if (cfg.getBoolean("container", "slave", false)
+ && !cfg.getBoolean("index", "scheduledIndexer", "runOnStartup", true)) {
+ return;
+ }
+
+ Stream<GroupReference> allGroupReferences;
+ try {
+ allGroupReferences = groups.getAllGroupReferences();
+ } catch (ConfigInvalidException | IOException e) {
+ throw new IllegalStateException("Unable to reindex groups, tests may fail", e);
+ }
+
+ allGroupReferences.forEach(
+ group -> {
+ try {
+ groupIndexer.index(group.getUUID());
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ String.format("Unable to index %s, tests may fail", group), e);
+ }
+ });
+ }
+
+ @Override
+ public void stop() {}
+}
diff --git a/java/com/google/gerrit/acceptance/ReindexProjectsAtStartup.java b/java/com/google/gerrit/acceptance/ReindexProjectsAtStartup.java
new file mode 100644
index 0000000..4893efa
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/ReindexProjectsAtStartup.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.index.project.ProjectIndexer;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Scopes;
+import java.io.IOException;
+
+/** Reindex all projects at Gerrit daemon startup. */
+public class ReindexProjectsAtStartup implements LifecycleListener {
+ private final ProjectIndexer projectIndexer;
+ private final GitRepositoryManager repoMgr;
+
+ public static class Module extends LifecycleModule {
+ @Override
+ protected void configure() {
+ listener().to(ReindexProjectsAtStartup.class).in(Scopes.SINGLETON);
+ }
+ }
+
+ @Inject
+ public ReindexProjectsAtStartup(ProjectIndexer projectIndexer, GitRepositoryManager repoMgr) {
+ this.projectIndexer = projectIndexer;
+ this.repoMgr = repoMgr;
+ }
+
+ @Override
+ public void start() {
+ repoMgr
+ .list()
+ .stream()
+ .forEach(
+ projectName -> {
+ try {
+ projectIndexer.index(projectName);
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ String.format("Unable to index %s, tests may fail", projectName), e);
+ }
+ });
+ }
+
+ @Override
+ public void stop() {}
+}
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
index e597ed0..17d9294 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperations.java
@@ -68,4 +68,11 @@
* @return the previous request scope.
*/
AcceptanceTestRequestScope.Context setApiUserAnonymous();
+
+ /**
+ * Sets the Guice request scope to the internal server user.
+ *
+ * @return the previous request scope.
+ */
+ AcceptanceTestRequestScope.Context setApiUserInternal();
}
diff --git a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
index 27b71b9..5546422 100644
--- a/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/request/RequestScopeOperationsImpl.java
@@ -29,6 +29,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.IdentifiedUser.GenericFactory;
+import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -48,6 +49,7 @@
private final AccountOperations accountOperations;
private final IdentifiedUser.GenericFactory userFactory;
private final Provider<AnonymousUser> anonymousUserProvider;
+ private final InternalUser.Factory internalUserFactory;
private final InetSocketAddress sshAddress;
private final TestSshKeys testSshKeys;
@@ -58,6 +60,7 @@
AccountOperations accountOperations,
GenericFactory userFactory,
Provider<AnonymousUser> anonymousUserProvider,
+ InternalUser.Factory internalUserFactory,
@Nullable @TestSshServerAddress InetSocketAddress sshAddress,
TestSshKeys testSshKeys) {
this.atrScope = atrScope;
@@ -65,6 +68,7 @@
this.accountOperations = accountOperations;
this.userFactory = userFactory;
this.anonymousUserProvider = anonymousUserProvider;
+ this.internalUserFactory = internalUserFactory;
this.sshAddress = sshAddress;
this.testSshKeys = testSshKeys;
}
@@ -95,6 +99,11 @@
return atrScope.set(atrScope.newContext(null, anonymousUserProvider.get()));
}
+ @Override
+ public AcceptanceTestRequestScope.Context setApiUserInternal() {
+ return atrScope.set(atrScope.newContext(null, internalUserFactory.create()));
+ }
+
private IdentifiedUser createIdentifiedUser(Account.Id accountId) {
return userFactory.create(
accountCache
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index b69f8f9..6f9fac5 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -23,6 +23,7 @@
V6_3("6.3.*"),
V6_4("6.4.*"),
V6_5("6.5.*"),
+ V6_6("6.6.*"),
V7_0("7.0.*");
private final String version;
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index a6df45f..7d356bf 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -14,9 +14,11 @@
package com.google.gerrit.extensions.api.changes;
+import com.google.common.collect.ListMultimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.CherryPickChangeInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
@@ -150,6 +152,9 @@
RelatedChangesInfo related() throws RestApiException;
+ /** Returns votes on the revision. */
+ ListMultimap<String, ApprovalInfo> votes() throws RestApiException;
+
abstract class MergeListRequest {
private boolean addLinks;
private int uninterestingParent = 1;
@@ -361,6 +366,11 @@
}
@Override
+ public ListMultimap<String, ApprovalInfo> votes() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public void description(String description) throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index 0139b52..3d70996 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -106,6 +106,8 @@
List<ProjectInfo> children(boolean recursive) throws RestApiException;
+ List<ProjectInfo> children(int limit) throws RestApiException;
+
ChildProjectApi child(String name) throws RestApiException;
/**
@@ -285,6 +287,11 @@
}
@Override
+ public List<ProjectInfo> children(int limit) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public ChildProjectApi child(String name) throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/client/ListChangesOption.java b/java/com/google/gerrit/extensions/client/ListChangesOption.java
index 6382686..5e4a3a7 100644
--- a/java/com/google/gerrit/extensions/client/ListChangesOption.java
+++ b/java/com/google/gerrit/extensions/client/ListChangesOption.java
@@ -78,7 +78,6 @@
TRACKING_IDS(21),
/** Skip mergeability data */
- @Deprecated
SKIP_MERGEABLE(22);
private final int value;
diff --git a/java/com/google/gerrit/extensions/common/ApprovalInfo.java b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
index 703235d..e40004b 100644
--- a/java/com/google/gerrit/extensions/common/ApprovalInfo.java
+++ b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -14,6 +14,7 @@
package com.google.gerrit.extensions.common;
+import com.google.gerrit.common.Nullable;
import java.sql.Timestamp;
public class ApprovalInfo extends AccountInfo {
@@ -28,7 +29,11 @@
}
public ApprovalInfo(
- Integer id, Integer value, VotingRangeInfo permittedVotingRange, String tag, Timestamp date) {
+ Integer id,
+ Integer value,
+ @Nullable VotingRangeInfo permittedVotingRange,
+ @Nullable String tag,
+ Timestamp date) {
super(id);
this.value = value;
this.permittedVotingRange = permittedVotingRange;
diff --git a/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index ab7bfdf..1eaaba3 100644
--- a/java/com/google/gerrit/httpd/HttpLogoutServlet.java
+++ b/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -19,9 +19,9 @@
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.server.AuditEvent;
import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.audit.AuditService;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.group.GroupAuditService;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -38,14 +38,14 @@
private final DynamicItem<WebSession> webSession;
private final Provider<String> urlProvider;
private final String logoutUrl;
- private final AuditService audit;
+ private final GroupAuditService audit;
@Inject
protected HttpLogoutServlet(
AuthConfig authConfig,
DynamicItem<WebSession> webSession,
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
- AuditService audit) {
+ GroupAuditService audit) {
this.webSession = webSession;
this.urlProvider = urlProvider;
this.logoutUrl = authConfig.getLogoutURL();
diff --git a/java/com/google/gerrit/httpd/RunAsFilter.java b/java/com/google/gerrit/httpd/RunAsFilter.java
index f3bf5af..1ff8580 100644
--- a/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -21,6 +21,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResolver;
@@ -103,19 +104,18 @@
return;
}
- Account target;
+ Account.Id target;
try {
- target = accountResolver.find(runas);
+ target = accountResolver.resolve(runas).asUnique().getAccount().getId();
+ } catch (UnprocessableEntityException e) {
+ replyError(req, res, SC_FORBIDDEN, "no account matches " + RUN_AS, null);
+ return;
} catch (OrmException | IOException | ConfigInvalidException e) {
logger.atWarning().withCause(e).log("cannot resolve account for %s", RUN_AS);
replyError(req, res, SC_INTERNAL_SERVER_ERROR, "cannot resolve " + RUN_AS, e);
return;
}
- if (target == null) {
- replyError(req, res, SC_FORBIDDEN, "no account matches " + RUN_AS, null);
- return;
- }
- session.get().setUserAccountId(target.getId());
+ session.get().setUserAccountId(target);
}
chain.doFilter(req, res);
diff --git a/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java b/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java
index 6e32980..107a07e 100644
--- a/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java
+++ b/java/com/google/gerrit/httpd/UniversalWebLoginFilter.java
@@ -87,7 +87,7 @@
}
private Optional<IdentifiedUser> loggedInUser() {
- return session.call(s -> s.isSignedIn())
+ return session.call(WebSession::isSignedIn)
? Optional.of(userProvider.get().asIdentifiedUser())
: Optional.empty();
}
diff --git a/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java b/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
index d25ff60..f468ecb 100644
--- a/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
+++ b/java/com/google/gerrit/httpd/auth/oauth/OAuthLogoutServlet.java
@@ -18,9 +18,9 @@
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.httpd.HttpLogoutServlet;
import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.server.audit.AuditService;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.group.GroupAuditService;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -39,7 +39,7 @@
AuthConfig authConfig,
DynamicItem<WebSession> webSession,
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
- AuditService audit,
+ GroupAuditService audit,
Provider<OAuthSession> oauthSession) {
super(authConfig, webSession, urlProvider, audit);
this.oauthSession = oauthSession;
diff --git a/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java b/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
index 8299c16..d75805c 100644
--- a/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
+++ b/java/com/google/gerrit/httpd/auth/openid/OAuthOverOpenIDLogoutServlet.java
@@ -18,9 +18,9 @@
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.httpd.HttpLogoutServlet;
import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.server.audit.AuditService;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.group.GroupAuditService;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -39,7 +39,7 @@
AuthConfig authConfig,
DynamicItem<WebSession> webSession,
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
- AuditService audit,
+ GroupAuditService audit,
Provider<OAuthSessionOverOpenID> oauthSession) {
super(authConfig, webSession, urlProvider, audit);
this.oauthSession = oauthSession;
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index 4057d54..2b00d7c 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -105,10 +105,10 @@
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.OptionUtil;
-import com.google.gerrit.server.audit.AuditService;
import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
import com.google.gerrit.server.cache.PerThreadCache;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.GroupAuditService;
import com.google.gerrit.server.logging.RequestId;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.permissions.GlobalPermission;
@@ -225,7 +225,7 @@
final DynamicItem<WebSession> webSession;
final Provider<ParameterParser> paramParser;
final PermissionBackend permissionBackend;
- final AuditService auditService;
+ final GroupAuditService auditService;
final RestApiMetrics metrics;
final Pattern allowOrigin;
final RestApiQuotaEnforcer quotaChecker;
@@ -236,7 +236,7 @@
DynamicItem<WebSession> webSession,
Provider<ParameterParser> paramParser,
PermissionBackend permissionBackend,
- AuditService auditService,
+ GroupAuditService auditService,
RestApiMetrics metrics,
RestApiQuotaEnforcer quotaChecker,
@GerritServerConfig Config cfg) {
diff --git a/java/com/google/gerrit/index/project/ProjectField.java b/java/com/google/gerrit/index/project/ProjectField.java
index 5e484b2..53624f2 100644
--- a/java/com/google/gerrit/index/project/ProjectField.java
+++ b/java/com/google/gerrit/index/project/ProjectField.java
@@ -49,7 +49,7 @@
exact("state").stored().build(p -> p.getProject().getState().name());
public static final FieldDef<ProjectData, Iterable<String>> ANCESTOR_NAME =
- exact("ancestor_name").buildRepeatable(p -> p.getParentNames());
+ exact("ancestor_name").buildRepeatable(ProjectData::getParentNames);
/**
* All values of all refs that were used in the course of indexing this document. This covers
diff --git a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index d9ca76d..ae36b48 100644
--- a/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -30,6 +30,7 @@
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.FieldType;
import com.google.gerrit.index.Index;
@@ -312,6 +313,14 @@
return result;
}
+ /**
+ * Trasform an index document into a target object type.
+ *
+ * @param doc index document
+ * @return target object, or null if the target object was not found or failed to load from the
+ * underlying store.
+ */
+ @Nullable
protected abstract V fromDocument(Document doc);
void add(Document doc, Values<V> values) {
diff --git a/java/com/google/gerrit/lucene/LuceneProjectIndex.java b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
index 807c40a..02d8655 100644
--- a/java/com/google/gerrit/lucene/LuceneProjectIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneProjectIndex.java
@@ -31,6 +31,7 @@
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.IndexUtils;
import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
@@ -138,6 +139,7 @@
@Override
protected ProjectData fromDocument(Document doc) {
Project.NameKey nameKey = new Project.NameKey(doc.getField(NAME.getName()).stringValue());
- return projectCache.get().get(nameKey).toProjectData();
+ ProjectState projectState = projectCache.get().get(nameKey);
+ return projectState == null ? null : projectState.toProjectData();
}
}
diff --git a/java/com/google/gerrit/pgm/Daemon.java b/java/com/google/gerrit/pgm/Daemon.java
index c280a2d..e2fd7f3 100644
--- a/java/com/google/gerrit/pgm/Daemon.java
+++ b/java/com/google/gerrit/pgm/Daemon.java
@@ -14,9 +14,11 @@
package com.google.gerrit.pgm;
+import static com.google.gerrit.common.Version.getVersion;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
@@ -118,6 +120,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.eclipse.jgit.lib.Config;
@@ -182,7 +185,7 @@
private boolean inMemoryTest;
private AbstractModule luceneModule;
private Module emailModule;
- private Module testSysModule;
+ private List<Module> testSysModules = new ArrayList<>();
private Module auditEventModule;
private Runnable serverStarted;
@@ -309,8 +312,8 @@
}
@VisibleForTesting
- public void setAdditionalSysModuleForTesting(@Nullable Module m) {
- testSysModule = m;
+ public void addAdditionalSysModuleForTesting(@Nullable Module... modules) {
+ testSysModules.addAll(Arrays.asList(modules));
}
@VisibleForTesting
@@ -363,7 +366,15 @@
}
private String myVersion() {
- return com.google.gerrit.common.Version.getVersion();
+ List<String> versionParts = new ArrayList<>();
+ if (slave) {
+ versionParts.add("[slave]");
+ }
+ if (headless) {
+ versionParts.add("[headless]");
+ }
+ versionParts.add(getVersion());
+ return Joiner.on(" ").join(versionParts);
}
private Injector createCfgInjector() {
@@ -461,9 +472,7 @@
modules.add(new AccountDeactivator.Module());
modules.add(new ChangeCleanupRunner.Module());
}
- if (testSysModule != null) {
- modules.add(testSysModule);
- }
+ modules.addAll(testSysModules);
modules.add(new LocalMergeSuperSetComputation.Module());
modules.add(new DefaultProjectNameLockManager.Module());
return cfgInjector.createChildInjector(
diff --git a/java/com/google/gerrit/reviewdb/client/Project.java b/java/com/google/gerrit/reviewdb/client/Project.java
index 0200c28..6e0e5c9 100644
--- a/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/java/com/google/gerrit/reviewdb/client/Project.java
@@ -95,8 +95,6 @@
protected String localDefaultDashboardId;
- protected String themeName;
-
protected String configRefState;
protected Project() {}
@@ -182,22 +180,6 @@
this.localDefaultDashboardId = localDefaultDashboardId;
}
- public String getThemeName() {
- return themeName;
- }
-
- public void setThemeName(String themeName) {
- this.themeName = themeName;
- }
-
- public void copySettingsFrom(Project update) {
- description = update.description;
- booleanConfigs = new HashMap<>(update.booleanConfigs);
- submitType = update.submitType;
- state = update.state;
- maxObjectSizeLimit = update.maxObjectSizeLimit;
- }
-
/**
* Returns the name key of the parent project.
*
diff --git a/java/com/google/gerrit/reviewdb/client/RefNames.java b/java/com/google/gerrit/reviewdb/client/RefNames.java
index 75b233f..91a5624 100644
--- a/java/com/google/gerrit/reviewdb/client/RefNames.java
+++ b/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -137,6 +137,11 @@
return false;
}
+ /** True if the provided ref is in {@code refs/changes/*}. */
+ public static boolean isRefsChanges(String ref) {
+ return ref.startsWith(REFS_CHANGES);
+ }
+
public static String refsGroups(AccountGroup.UUID groupUuid) {
return REFS_GROUPS + shardUuid(groupUuid.get());
}
diff --git a/java/com/google/gerrit/server/StartupChecks.java b/java/com/google/gerrit/server/StartupChecks.java
index 5ece91d..9bf94ae 100644
--- a/java/com/google/gerrit/server/StartupChecks.java
+++ b/java/com/google/gerrit/server/StartupChecks.java
@@ -44,7 +44,7 @@
@Override
public void start() throws StartupException {
- startupChecks.runEach(c -> c.check(), StartupException.class);
+ startupChecks.runEach(StartupCheck::check, StartupException.class);
}
@Override
diff --git a/java/com/google/gerrit/server/account/AccountResolver.java b/java/com/google/gerrit/server/account/AccountResolver.java
index 38aed19..9b4952b 100644
--- a/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/java/com/google/gerrit/server/account/AccountResolver.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2019 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,272 +14,603 @@
package com.google.gerrit.server.account;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
-import static java.util.stream.Collectors.toSet;
+import static java.util.Comparator.comparing;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.index.Schema;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.util.Collections;
-import java.util.HashSet;
+import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
+/**
+ * Helper for resolving accounts given arbitrary user-provided input.
+ *
+ * <p>The {@code resolve*} methods each define a list of accepted formats for account resolution.
+ * The algorithm for resolving accounts from a list of formats is as follows:
+ *
+ * <ol>
+ * <li>For each recognized format in the order listed in the method Javadoc, check whether the
+ * input matches that format.
+ * <li>If so, resolve accounts according to that format.
+ * <li>Filter out invisible and inactive accounts.
+ * <li>If the result list is non-empty, return.
+ * <li>If the format is listed above as being short-circuiting, return.
+ * <li>Otherwise, return to step 1 with the next format.
+ * </ol>
+ *
+ * <p>The result never includes accounts that are not visible to the calling user. It also never
+ * includes inactive accounts, with a small number of specific exceptions noted in method Javadoc.
+ */
@Singleton
public class AccountResolver {
- private final Provider<CurrentUser> self;
- private final Realm realm;
- private final Accounts accounts;
- private final AccountCache byId;
- private final IdentifiedUser.GenericFactory userFactory;
+ public static class UnresolvableAccountException extends UnprocessableEntityException {
+ private static final long serialVersionUID = 1L;
+ private final Result result;
+
+ @VisibleForTesting
+ UnresolvableAccountException(Result result) {
+ super(exceptionMessage(result));
+ this.result = result;
+ }
+
+ public boolean isSelf() {
+ return result.isSelf();
+ }
+ }
+
+ public static String exceptionMessage(Result result) {
+ checkArgument(result.asList().size() != 1);
+ if (result.asList().isEmpty()) {
+ if (result.isSelf()) {
+ return "Resolving account '" + result.input() + "' requires login";
+ }
+ if (result.filteredInactive().isEmpty()) {
+ return "Account '" + result.input() + "' not found";
+ }
+ return result
+ .filteredInactive()
+ .stream()
+ .map(a -> formatForException(result, a))
+ .collect(
+ joining(
+ "\n",
+ "Account '"
+ + result.input()
+ + "' only matches inactive accounts. To use an inactive account, retry with"
+ + " one of the following exact account IDs:\n",
+ ""));
+ }
+
+ return result
+ .asList()
+ .stream()
+ .map(a -> formatForException(result, a))
+ .collect(joining("\n", "Account '" + result.input() + "' is ambiguous:\n", ""));
+ }
+
+ private static String formatForException(Result result, AccountState state) {
+ return state.getAccount().getId()
+ + ": "
+ + state.getAccount().getNameEmail(result.accountResolver().anonymousCowardName);
+ }
+
+ public static boolean isSelf(String input) {
+ return "self".equals(input) || "me".equals(input);
+ }
+
+ public class Result {
+ private final String input;
+ private final ImmutableList<AccountState> list;
+ private final ImmutableList<AccountState> filteredInactive;
+
+ @VisibleForTesting
+ Result(String input, List<AccountState> list, List<AccountState> filteredInactive) {
+ this.input = requireNonNull(input);
+ this.list = canonicalize(list);
+ this.filteredInactive = canonicalize(filteredInactive);
+ }
+
+ private ImmutableList<AccountState> canonicalize(List<AccountState> list) {
+ TreeSet<AccountState> set = new TreeSet<>(comparing(a -> a.getAccount().getId().get()));
+ set.addAll(requireNonNull(list));
+ return ImmutableList.copyOf(set);
+ }
+
+ public String input() {
+ return input;
+ }
+
+ public boolean isSelf() {
+ return AccountResolver.isSelf(input);
+ }
+
+ public ImmutableList<AccountState> asList() {
+ return list;
+ }
+
+ public ImmutableSet<Account.Id> asNonEmptyIdSet() throws UnresolvableAccountException {
+ if (list.isEmpty()) {
+ throw new UnresolvableAccountException(this);
+ }
+ return asIdSet();
+ }
+
+ public ImmutableSet<Account.Id> asIdSet() {
+ return list.stream().map(a -> a.getAccount().getId()).collect(toImmutableSet());
+ }
+
+ public AccountState asUnique() throws UnresolvableAccountException {
+ ensureUnique();
+ return list.get(0);
+ }
+
+ private void ensureUnique() throws UnresolvableAccountException {
+ if (list.size() != 1) {
+ throw new UnresolvableAccountException(this);
+ }
+ }
+
+ public IdentifiedUser asUniqueUser() throws UnresolvableAccountException {
+ ensureUnique();
+ if (isSelf()) {
+ // In the special case of "self", use the exact IdentifiedUser from the request context, to
+ // preserve the peer address and any other per-request state.
+ return self.get().asIdentifiedUser();
+ }
+ return userFactory.create(asUnique());
+ }
+
+ public IdentifiedUser asUniqueUserOnBehalfOf(CurrentUser caller)
+ throws UnresolvableAccountException {
+ ensureUnique();
+ if (isSelf()) {
+ // TODO(dborowitz): This preserves old behavior, but it seems wrong to discard the caller.
+ return self.get().asIdentifiedUser();
+ }
+ return userFactory.runAs(
+ null, list.get(0).getAccount().getId(), requireNonNull(caller).getRealUser());
+ }
+
+ @VisibleForTesting
+ ImmutableList<AccountState> filteredInactive() {
+ return filteredInactive;
+ }
+
+ private AccountResolver accountResolver() {
+ return AccountResolver.this;
+ }
+ }
+
+ @VisibleForTesting
+ interface Searcher<I> {
+ default boolean callerShouldFilterOutInactiveCandidates() {
+ return true;
+ }
+
+ default boolean callerMayAssumeCandidatesAreVisible() {
+ return false;
+ }
+
+ Optional<I> tryParse(String input) throws IOException, OrmException;
+
+ Stream<AccountState> search(I input) throws OrmException, IOException, ConfigInvalidException;
+
+ boolean shortCircuitIfNoResults();
+
+ default Optional<Stream<AccountState>> trySearch(String input)
+ throws OrmException, IOException, ConfigInvalidException {
+ Optional<I> parsed = tryParse(input);
+ return parsed.isPresent() ? Optional.of(search(parsed.get())) : Optional.empty();
+ }
+ }
+
+ @VisibleForTesting
+ abstract static class StringSearcher implements Searcher<String> {
+ @Override
+ public final Optional<String> tryParse(String input) {
+ return matches(input) ? Optional.of(input) : Optional.empty();
+ }
+
+ protected abstract boolean matches(String input);
+ }
+
+ private abstract class AccountIdSearcher implements Searcher<Account.Id> {
+ @Override
+ public final Stream<AccountState> search(Account.Id input) {
+ return Streams.stream(accountCache.get(input));
+ }
+ }
+
+ private class BySelf extends StringSearcher {
+ @Override
+ public boolean callerShouldFilterOutInactiveCandidates() {
+ return false;
+ }
+
+ @Override
+ public boolean callerMayAssumeCandidatesAreVisible() {
+ return true;
+ }
+
+ @Override
+ protected boolean matches(String input) {
+ return "self".equals(input) || "me".equals(input);
+ }
+
+ @Override
+ public Stream<AccountState> search(String input) {
+ CurrentUser user = self.get();
+ if (!user.isIdentifiedUser()) {
+ return Stream.empty();
+ }
+ return Stream.of(user.asIdentifiedUser().state());
+ }
+
+ @Override
+ public boolean shortCircuitIfNoResults() {
+ return true;
+ }
+ }
+
+ private class ByExactAccountId extends AccountIdSearcher {
+ @Override
+ public boolean callerShouldFilterOutInactiveCandidates() {
+ return false;
+ }
+
+ @Override
+ public Optional<Account.Id> tryParse(String input) {
+ return Account.Id.tryParse(input);
+ }
+
+ @Override
+ public boolean shortCircuitIfNoResults() {
+ return true;
+ }
+ }
+
+ private class ByParenthesizedAccountId extends AccountIdSearcher {
+ private final Pattern pattern = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$");
+
+ @Override
+ public Optional<Account.Id> tryParse(String input) {
+ Matcher m = pattern.matcher(input);
+ return m.matches() ? Account.Id.tryParse(m.group(1)) : Optional.empty();
+ }
+
+ @Override
+ public boolean shortCircuitIfNoResults() {
+ return true;
+ }
+ }
+
+ private class ByUsername extends StringSearcher {
+ @Override
+ public boolean matches(String input) {
+ return ExternalId.isValidUsername(input);
+ }
+
+ @Override
+ public Stream<AccountState> search(String input) {
+ return Streams.stream(accountCache.getByUsername(input));
+ }
+
+ @Override
+ public boolean shortCircuitIfNoResults() {
+ return false;
+ }
+ }
+
+ private class ByNameAndEmail extends StringSearcher {
+ @Override
+ protected boolean matches(String input) {
+ int lt = input.indexOf('<');
+ int gt = input.indexOf('>');
+ return lt >= 0 && gt > lt && input.contains("@");
+ }
+
+ @Override
+ public Stream<AccountState> search(String nameOrEmail) throws OrmException, IOException {
+ // TODO(dborowitz): This would probably work as a Searcher<Address>
+ int lt = nameOrEmail.indexOf('<');
+ int gt = nameOrEmail.indexOf('>');
+ Set<Account.Id> ids = emails.getAccountFor(nameOrEmail.substring(lt + 1, gt));
+ ImmutableList<AccountState> allMatches = toAccountStates(ids).collect(toImmutableList());
+ if (allMatches.isEmpty() || allMatches.size() == 1) {
+ return allMatches.stream();
+ }
+
+ // More than one match. If there are any that match the full name as well, return only that
+ // subset. Otherwise, all are equally non-matching, so return the full set.
+ String name = nameOrEmail.substring(0, lt - 1);
+ ImmutableList<AccountState> nameMatches =
+ allMatches
+ .stream()
+ .filter(a -> name.equals(a.getAccount().getFullName()))
+ .collect(toImmutableList());
+ return !nameMatches.isEmpty() ? nameMatches.stream() : allMatches.stream();
+ }
+
+ @Override
+ public boolean shortCircuitIfNoResults() {
+ return true;
+ }
+ }
+
+ private class ByEmail extends StringSearcher {
+ @Override
+ protected boolean matches(String input) {
+ return input.contains("@");
+ }
+
+ @Override
+ public Stream<AccountState> search(String input) throws OrmException, IOException {
+ return toAccountStates(emails.getAccountFor(input));
+ }
+
+ @Override
+ public boolean shortCircuitIfNoResults() {
+ return true;
+ }
+ }
+
+ private class FromRealm extends AccountIdSearcher {
+ @Override
+ public Optional<Account.Id> tryParse(String input) throws IOException {
+ return Optional.ofNullable(realm.lookup(input));
+ }
+
+ @Override
+ public boolean shortCircuitIfNoResults() {
+ return false;
+ }
+ }
+
+ private class ByFullName implements Searcher<AccountState> {
+ @Override
+ public boolean callerMayAssumeCandidatesAreVisible() {
+ return true; // Rely on enforceVisibility from the index.
+ }
+
+ @Override
+ public Optional<AccountState> tryParse(String input) throws OrmException {
+ List<AccountState> results =
+ accountQueryProvider.get().enforceVisibility(true).byFullName(input);
+ return results.size() == 1 ? Optional.of(results.get(0)) : Optional.empty();
+ }
+
+ @Override
+ public Stream<AccountState> search(AccountState input) {
+ return Stream.of(input);
+ }
+
+ @Override
+ public boolean shortCircuitIfNoResults() {
+ return false;
+ }
+ }
+
+ private class ByDefaultSearch extends StringSearcher {
+ @Override
+ public boolean callerMayAssumeCandidatesAreVisible() {
+ return true; // Rely on enforceVisibility from the index.
+ }
+
+ @Override
+ protected boolean matches(String input) {
+ return true;
+ }
+
+ @Override
+ public Stream<AccountState> search(String input) throws OrmException {
+ // At this point we have no clue. Just perform a whole bunch of suggestions and pray we come
+ // up with a reasonable result list.
+ // TODO(dborowitz): This doesn't match the documentation; consider whether it's possible to be
+ // more strict here.
+ return accountQueryProvider.get().enforceVisibility(true).byDefault(input).stream();
+ }
+
+ @Override
+ public boolean shortCircuitIfNoResults() {
+ // In practice this doesn't matter since this is the last searcher in the list, but considered
+ // on its own, it doesn't necessarily need to be terminal.
+ return false;
+ }
+ }
+
+ private final ImmutableList<Searcher<?>> nameOrEmailSearchers =
+ ImmutableList.of(
+ new ByNameAndEmail(),
+ new ByEmail(),
+ new FromRealm(),
+ new ByFullName(),
+ new ByDefaultSearch());
+
+ private final ImmutableList<Searcher<?>> searchers =
+ ImmutableList.<Searcher<?>>builder()
+ .add(new BySelf())
+ .add(new ByExactAccountId())
+ .add(new ByParenthesizedAccountId())
+ .add(new ByUsername())
+ .addAll(nameOrEmailSearchers)
+ .build();
+
+ private final AccountCache accountCache;
private final AccountControl.Factory accountControlFactory;
- private final Provider<InternalAccountQuery> accountQueryProvider;
private final Emails emails;
+ private final IdentifiedUser.GenericFactory userFactory;
+ private final Provider<CurrentUser> self;
+ private final Provider<InternalAccountQuery> accountQueryProvider;
+ private final Realm realm;
+ private final String anonymousCowardName;
@Inject
AccountResolver(
- Provider<CurrentUser> self,
- Realm realm,
- Accounts accounts,
- AccountCache byId,
- IdentifiedUser.GenericFactory userFactory,
+ AccountCache accountCache,
+ Emails emails,
AccountControl.Factory accountControlFactory,
+ IdentifiedUser.GenericFactory userFactory,
+ Provider<CurrentUser> self,
Provider<InternalAccountQuery> accountQueryProvider,
- Emails emails) {
- this.self = self;
+ Realm realm,
+ @AnonymousCowardName String anonymousCowardName) {
this.realm = realm;
- this.accounts = accounts;
- this.byId = byId;
- this.userFactory = userFactory;
+ this.accountCache = accountCache;
this.accountControlFactory = accountControlFactory;
+ this.userFactory = userFactory;
+ this.self = self;
this.accountQueryProvider = accountQueryProvider;
this.emails = emails;
+ this.anonymousCowardName = anonymousCowardName;
}
/**
- * Locate exactly one account matching the input string.
+ * Resolves all accounts matching the input string.
*
- * @param input a string of the format "Full Name <email@example>", just the email address
- * ("email@example"), a full name ("Full Name"), an account ID ("18419") or a user name
- * ("username").
- * @return the single account that matches; null if no account matches or there are multiple
- * candidates. If {@code input} is a numeric string, returns an account if and only if that
- * number corresponds to an actual account ID.
+ * <p>The following input formats are recognized:
+ *
+ * <ul>
+ * <li>The strings {@code "self"} and {@code "me"}, if the current user is an {@link
+ * IdentifiedUser}. In this case, may return exactly one inactive account.
+ * <li>A bare account ID ({@code "18419"}). In this case, may return exactly one inactive
+ * account. This case short-circuits if the input matches.
+ * <li>An account ID in parentheses following a full name ({@code "Full Name (18419)"}). This
+ * case short-circuits if the input matches.
+ * <li>A username ({@code "username"}).
+ * <li>A full name and email address ({@code "Full Name <email@example>"}). This case
+ * short-circuits if the input matches.
+ * <li>An email address ({@code "email@example"}. This case short-circuits if the input matches.
+ * <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}.
+ * <li>A full name ({@code "Full Name"}).
+ * <li>As a fallback, a {@link
+ * com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema,
+ * boolean, String) default search} against the account index.
+ * </ul>
+ *
+ * @param input input string.
+ * @return a result describing matching accounts. Never null even if the result set is empty.
+ * @throws OrmException if an error occurs.
+ * @throws ConfigInvalidException if an error occurs.
+ * @throws IOException if an error occurs.
*/
- public Account find(String input) throws OrmException, IOException, ConfigInvalidException {
- Set<Account.Id> r = findAll(input);
- if (r.size() == 1) {
- return byId.get(r.iterator().next()).map(AccountState::getAccount).orElse(null);
- }
+ public Result resolve(String input) throws OrmException, ConfigInvalidException, IOException {
+ return searchImpl(input, searchers, visibilitySupplier());
+ }
- Account match = null;
- for (Account.Id id : r) {
- Optional<Account> account = byId.get(id).map(AccountState::getAccount);
- if (!account.map(Account::isActive).orElse(false)) {
+ /**
+ * Resolves all accounts matching the input string by name or email.
+ *
+ * <p>The following input formats are recognized:
+ *
+ * <ul>
+ * <li>A full name and email address ({@code "Full Name <email@example>"}). This case
+ * short-circuits if the input matches.
+ * <li>An email address ({@code "email@example"}. This case short-circuits if the input matches.
+ * <li>An account name recognized by the configured {@link Realm#lookup(String)} Realm}.
+ * <li>A full name ({@code "Full Name"}).
+ * <li>As a fallback, a {@link
+ * com.google.gerrit.server.query.account.AccountPredicates#defaultPredicate(Schema,
+ * boolean, String) default search} against the account index.
+ * </ul>
+ *
+ * @param input input string.
+ * @return a result describing matching accounts. Never null even if the result set is empty.
+ * @throws OrmException if an error occurs.
+ * @throws ConfigInvalidException if an error occurs.
+ * @throws IOException if an error occurs.
+ * @deprecated for use only by MailUtil for parsing commit footers; that class needs to be
+ * reevaluated.
+ */
+ @Deprecated
+ public Result resolveByNameOrEmail(String input)
+ throws OrmException, ConfigInvalidException, IOException {
+ return searchImpl(input, nameOrEmailSearchers, visibilitySupplier());
+ }
+
+ private Supplier<Predicate<AccountState>> visibilitySupplier() {
+ return () -> accountControlFactory.get()::canSee;
+ }
+
+ @VisibleForTesting
+ Result searchImpl(
+ String input,
+ ImmutableList<Searcher<?>> searchers,
+ Supplier<Predicate<AccountState>> visibilitySupplier)
+ throws OrmException, ConfigInvalidException, IOException {
+ visibilitySupplier = Suppliers.memoize(visibilitySupplier::get);
+ List<AccountState> inactive = new ArrayList<>();
+
+ for (Searcher<?> searcher : searchers) {
+ Optional<Stream<AccountState>> maybeResults = searcher.trySearch(input);
+ if (!maybeResults.isPresent()) {
continue;
}
- if (match != null) {
- return null;
- }
- match = account.get();
- }
- return match;
- }
+ Stream<AccountState> results = maybeResults.get();
- /**
- * Find all accounts matching the input string.
- *
- * @param input a string of the format "Full Name <email@example>", just the email address
- * ("email@example"), a full name ("Full Name"), an account ID ("18419") or a user name
- * ("username").
- * @return the accounts that match, empty set if none. Never null. If {@code input} is a numeric
- * string, returns a singleton set if that number corresponds to a real account ID, and an
- * empty set otherwise if it does not.
- */
- public Set<Account.Id> findAll(String input)
- throws OrmException, IOException, ConfigInvalidException {
- Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(input);
- if (m.matches()) {
- Optional<Account.Id> id = Account.Id.tryParse(m.group(1));
- if (id.isPresent()) {
- return Streams.stream(accounts.get(id.get()))
- .map(a -> a.getAccount().getId())
- .collect(toImmutableSet());
- }
- }
-
- if (input.matches("^[1-9][0-9]*$")) {
- Optional<Account.Id> id = Account.Id.tryParse(input);
- if (id.isPresent()) {
- return Streams.stream(accounts.get(id.get()))
- .map(a -> a.getAccount().getId())
- .collect(toImmutableSet());
- }
- }
-
- if (ExternalId.isValidUsername(input)) {
- Optional<AccountState> who = byId.getByUsername(input);
- if (who.isPresent()) {
- return ImmutableSet.of(who.map(a -> a.getAccount().getId()).get());
- }
- }
-
- return findAllByNameOrEmail(input);
- }
-
- /**
- * Locate exactly one account matching the name or name/email string.
- *
- * @param nameOrEmail a string of the format "Full Name <email@example>", just the email
- * address ("email@example"), a full name ("Full Name").
- * @return the single account that matches; null if no account matches or there are multiple
- * candidates.
- */
- public Account findByNameOrEmail(String nameOrEmail) throws OrmException, IOException {
- Set<Account.Id> r = findAllByNameOrEmail(nameOrEmail);
- return r.size() == 1
- ? byId.get(r.iterator().next()).map(AccountState::getAccount).orElse(null)
- : null;
- }
-
- /**
- * Locate exactly one account matching the name or name/email string.
- *
- * @param nameOrEmail a string of the format "Full Name <email@example>", just the email
- * address ("email@example"), a full name ("Full Name").
- * @return the accounts that match, empty collection if none. Never null.
- */
- public Set<Account.Id> findAllByNameOrEmail(String nameOrEmail) throws OrmException, IOException {
- int lt = nameOrEmail.indexOf('<');
- int gt = nameOrEmail.indexOf('>');
- if (lt >= 0 && gt > lt && nameOrEmail.contains("@")) {
- Set<Account.Id> ids = emails.getAccountFor(nameOrEmail.substring(lt + 1, gt));
- if (ids.isEmpty() || ids.size() == 1) {
- return ids;
+ if (!searcher.callerMayAssumeCandidatesAreVisible()) {
+ results = results.filter(visibilitySupplier.get());
}
- // more than one match, try to return the best one
- String name = nameOrEmail.substring(0, lt - 1);
- Set<Account.Id> nameMatches = new HashSet<>();
- for (Account.Id id : ids) {
- Optional<Account> a = byId.get(id).map(AccountState::getAccount);
- if (a.isPresent() && name.equals(a.get().getFullName())) {
- nameMatches.add(id);
- }
- }
- return nameMatches.isEmpty() ? ids : nameMatches;
- }
-
- if (nameOrEmail.contains("@")) {
- return emails.getAccountFor(nameOrEmail);
- }
-
- Account.Id id = realm.lookup(nameOrEmail);
- if (id != null) {
- return Collections.singleton(id);
- }
-
- List<AccountState> m = accountQueryProvider.get().byFullName(nameOrEmail);
- if (m.size() == 1) {
- return Collections.singleton(m.get(0).getAccount().getId());
- }
-
- // At this point we have no clue. Just perform a whole bunch of suggestions
- // and pray we come up with a reasonable result list.
- // TODO(dborowitz): This doesn't match the documentation; consider whether it's possible to be
- // more strict here.
- return accountQueryProvider
- .get()
- .byDefault(nameOrEmail)
- .stream()
- .map(a -> a.getAccount().getId())
- .collect(toSet());
- }
-
- /**
- * Parses a account ID from a request body and returns the user.
- *
- * @param id ID of the account, can be a string of the format "{@code Full Name
- * <email@example.com>}", just the email address, a full name if it is unique, an account ID,
- * a user name or "{@code self}" for the calling user
- * @return the user, never null.
- * @throws UnprocessableEntityException thrown if the account ID cannot be resolved or if the
- * account is not visible to the calling user
- */
- public IdentifiedUser parse(String id)
- throws AuthException, UnprocessableEntityException, OrmException, IOException,
- ConfigInvalidException {
- return parseOnBehalfOf(null, id);
- }
-
- /**
- * Parses an account ID and returns the user without making any permission check whether the
- * current user can see the account.
- *
- * @param id ID of the account, can be a string of the format "{@code Full Name
- * <email@example.com>}", just the email address, a full name if it is unique, an account ID,
- * a user name or "{@code self}" for the calling user
- * @return the user, null if no user is found for the given account ID
- * @throws AuthException thrown if 'self' is used as account ID and the current user is not
- * authenticated
- * @throws OrmException
- * @throws ConfigInvalidException
- * @throws IOException
- */
- public IdentifiedUser parseId(String id)
- throws AuthException, OrmException, IOException, ConfigInvalidException {
- return parseIdOnBehalfOf(null, id);
- }
-
- /**
- * Like {@link #parse(String)}, but also sets the {@link CurrentUser#getRealUser()} on the result.
- */
- public IdentifiedUser parseOnBehalfOf(@Nullable CurrentUser caller, String id)
- throws AuthException, UnprocessableEntityException, OrmException, IOException,
- ConfigInvalidException {
- IdentifiedUser user = parseIdOnBehalfOf(caller, id);
- if (user == null || !accountControlFactory.get().canSee(user.getAccount().getId())) {
- throw new UnprocessableEntityException(
- String.format("Account '%s' is not found or ambiguous", id));
- }
- return user;
- }
-
- private IdentifiedUser parseIdOnBehalfOf(@Nullable CurrentUser caller, String id)
- throws AuthException, OrmException, IOException, ConfigInvalidException {
- if (id.equals("self")) {
- CurrentUser user = self.get();
- if (user.isIdentifiedUser()) {
- return user.asIdentifiedUser();
- } else if (user instanceof AnonymousUser) {
- throw new AuthException("Authentication required");
+ List<AccountState> list;
+ if (searcher.callerShouldFilterOutInactiveCandidates()) {
+ // Keep track of all inactive candidates discovered by any searchers. If we end up short-
+ // circuiting, the inactive list will be discarded.
+ List<AccountState> active = new ArrayList<>();
+ results.forEach(a -> (a.getAccount().isActive() ? active : inactive).add(a));
+ list = active;
} else {
- return null;
+ list = results.collect(toImmutableList());
+ }
+
+ if (!list.isEmpty()) {
+ return createResult(input, list);
+ }
+ if (searcher.shortCircuitIfNoResults()) {
+ // For a short-circuiting searcher, return results even if empty.
+ return !inactive.isEmpty() ? emptyResult(input, inactive) : createResult(input, list);
}
}
+ return emptyResult(input, inactive);
+ }
- Account match = find(id);
- if (match == null) {
- return null;
- }
- CurrentUser realUser = caller != null ? caller.getRealUser() : null;
- return userFactory.runAs(null, match.getId(), realUser);
+ private Result createResult(String input, List<AccountState> list) {
+ return new Result(input, list, ImmutableList.of());
+ }
+
+ private Result emptyResult(String input, List<AccountState> inactive) {
+ return new Result(input, ImmutableList.of(), inactive);
+ }
+
+ private Stream<AccountState> toAccountStates(Set<Account.Id> ids) {
+ return accountCache.get(ids).values().stream();
}
}
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 897d673..3239415 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -24,7 +24,6 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Runnables;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.git.RefUpdateUtil;
import com.google.gerrit.reviewdb.client.Account;
@@ -44,9 +43,9 @@
import com.google.gerrit.server.update.RetryHelper.ActionType;
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 com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.List;
@@ -123,16 +122,28 @@
public interface Factory {
/**
* Creates an {@code AccountsUpdate} which uses the identity of the specified user as author for
- * all commits related to accounts. The Gerrit server identity will be used as committer.
+ * all commits related to accounts. The server identity will be used as committer.
*
- * <p><strong>Note</strong>: Please use this method with care and rather consider to use the
- * correct annotation on the provider of an {@code AccountsUpdate} instead.
+ * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
+ * com.google.gerrit.server.UserInitiated} annotation on the provider of an {@code
+ * AccountsUpdate} instead.
*
- * @param currentUser the user to which modifications should be attributed, or {@code null} if
- * the Gerrit server identity should also be used as author
+ * @param currentUser the user to which modifications should be attributed
+ * @param externalIdNotesLoader the loader that should be used to load external ID notes
*/
- AccountsUpdate create(
- @Nullable IdentifiedUser currentUser, ExternalIdNotesLoader externalIdNotesLoader);
+ AccountsUpdate create(IdentifiedUser currentUser, ExternalIdNotesLoader externalIdNotesLoader);
+
+ /**
+ * Creates an {@code AccountsUpdate} which uses the server identity as author and committer for
+ * all commits related to accounts.
+ *
+ * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
+ * com.google.gerrit.server.ServerInitiated} annotation on the provider of an {@code
+ * AccountsUpdate} instead.
+ *
+ * @param externalIdNotesLoader the loader that should be used to load external ID notes
+ */
+ AccountsUpdate createWithServerIdent(ExternalIdNotesLoader externalIdNotesLoader);
}
/**
@@ -172,7 +183,7 @@
private final GitRepositoryManager repoManager;
private final GitReferenceUpdated gitRefUpdated;
- @Nullable private final IdentifiedUser currentUser;
+ private final Optional<IdentifiedUser> currentUser;
private final AllUsersName allUsersName;
private final ExternalIds externalIds;
private final Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory;
@@ -187,7 +198,7 @@
// Invoked after updating the account but before committing the changes.
private final Runnable beforeCommit;
- @Inject
+ @AssistedInject
AccountsUpdate(
GitRepositoryManager repoManager,
GitReferenceUpdated gitRefUpdated,
@@ -196,19 +207,44 @@
Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
RetryHelper retryHelper,
@GerritPersonIdent PersonIdent serverIdent,
- @Assisted @Nullable IdentifiedUser currentUser,
@Assisted ExternalIdNotesLoader extIdNotesLoader) {
this(
repoManager,
gitRefUpdated,
- currentUser,
+ Optional.empty(),
allUsersName,
externalIds,
metaDataUpdateInternalFactory,
retryHelper,
extIdNotesLoader,
serverIdent,
- createPersonIdent(serverIdent, currentUser),
+ createPersonIdent(serverIdent, Optional.empty()),
+ Runnables.doNothing(),
+ Runnables.doNothing());
+ }
+
+ @AssistedInject
+ AccountsUpdate(
+ GitRepositoryManager repoManager,
+ GitReferenceUpdated gitRefUpdated,
+ AllUsersName allUsersName,
+ ExternalIds externalIds,
+ Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
+ RetryHelper retryHelper,
+ @GerritPersonIdent PersonIdent serverIdent,
+ @Assisted IdentifiedUser currentUser,
+ @Assisted ExternalIdNotesLoader extIdNotesLoader) {
+ this(
+ repoManager,
+ gitRefUpdated,
+ Optional.of(currentUser),
+ allUsersName,
+ externalIds,
+ metaDataUpdateInternalFactory,
+ retryHelper,
+ extIdNotesLoader,
+ serverIdent,
+ createPersonIdent(serverIdent, Optional.of(currentUser)),
Runnables.doNothing(),
Runnables.doNothing());
}
@@ -217,7 +253,7 @@
public AccountsUpdate(
GitRepositoryManager repoManager,
GitReferenceUpdated gitRefUpdated,
- @Nullable IdentifiedUser currentUser,
+ Optional<IdentifiedUser> currentUser,
AllUsersName allUsersName,
ExternalIds externalIds,
Provider<MetaDataUpdate.InternalFactory> metaDataUpdateInternalFactory,
@@ -243,11 +279,11 @@
}
private static PersonIdent createPersonIdent(
- PersonIdent serverIdent, @Nullable IdentifiedUser user) {
- if (user == null) {
+ PersonIdent serverIdent, Optional<IdentifiedUser> user) {
+ if (!user.isPresent()) {
return serverIdent;
}
- return user.newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone());
+ return user.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone());
}
/**
@@ -455,7 +491,7 @@
.updateCaches(accountsThatWillBeReindexByReindexAfterRefUpdate);
gitRefUpdated.fire(
- allUsersName, batchRefUpdate, currentUser != null ? currentUser.state() : null);
+ allUsersName, batchRefUpdate, currentUser.map(user -> user.state()).orElse(null));
}
private static Set<Account.Id> getUpdatedAccounts(BatchRefUpdate batchRefUpdate) {
diff --git a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
index bfe46d2..4e91e0b 100644
--- a/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/AllExternalIds.java
@@ -39,7 +39,7 @@
static AllExternalIds create(Collection<ExternalId> externalIds) {
return new AutoValue_AllExternalIds(
- externalIds.stream().collect(toImmutableSetMultimap(e -> e.accountId(), e -> e)),
+ externalIds.stream().collect(toImmutableSetMultimap(ExternalId::accountId, e -> e)),
byEmailCopy(externalIds));
}
@@ -48,7 +48,7 @@
return externalIds
.stream()
.filter(e -> !Strings.isNullOrEmpty(e.email()))
- .collect(toImmutableSetMultimap(e -> e.email(), e -> e));
+ .collect(toImmutableSetMultimap(ExternalId::email, e -> e));
}
public abstract ImmutableSetMultimap<Account.Id, ExternalId> byAccount();
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index f8a2ecb..2df7ae6 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -18,6 +18,8 @@
import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder.ListMultimapBuilder;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.Changes;
@@ -36,6 +38,7 @@
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ActionInfo;
+import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.CherryPickChangeInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
@@ -51,6 +54,10 @@
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.account.AccountDirectory.FillOptions;
+import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.change.FileResource;
import com.google.gerrit.server.change.RebaseUtil;
import com.google.gerrit.server.change.RevisionResource;
@@ -85,6 +92,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
+import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -135,6 +143,8 @@
private final GetRelated getRelated;
private final PutDescription putDescription;
private final GetDescription getDescription;
+ private final ApprovalsUtil approvalsUtil;
+ private final AccountLoader.Factory accountLoaderFactory;
@Inject
RevisionApiImpl(
@@ -176,6 +186,8 @@
GetRelated getRelated,
PutDescription putDescription,
GetDescription getDescription,
+ ApprovalsUtil approvalsUtil,
+ AccountLoader.Factory accountLoaderFactory,
@Assisted RevisionResource r) {
this.repoManager = repoManager;
this.changes = changes;
@@ -215,6 +227,8 @@
this.getRelated = getRelated;
this.putDescription = putDescription;
this.getDescription = getDescription;
+ this.approvalsUtil = approvalsUtil;
+ this.accountLoaderFactory = accountLoaderFactory;
this.revision = r;
}
@@ -568,6 +582,36 @@
}
@Override
+ public ListMultimap<String, ApprovalInfo> votes() throws RestApiException {
+ ListMultimap<String, ApprovalInfo> result =
+ ListMultimapBuilder.treeKeys().arrayListValues().build();
+ try {
+ Iterable<PatchSetApproval> approvals =
+ approvalsUtil.byPatchSet(revision.getNotes(), revision.getPatchSet().getId(), null, null);
+ AccountLoader accountLoader =
+ accountLoaderFactory.create(
+ EnumSet.of(
+ FillOptions.ID, FillOptions.NAME, FillOptions.EMAIL, FillOptions.USERNAME));
+ for (PatchSetApproval approval : approvals) {
+ String label = approval.getLabel();
+ ApprovalInfo info =
+ new ApprovalInfo(
+ approval.getAccountId().get(),
+ Integer.valueOf(approval.getValue()),
+ null,
+ approval.getTag(),
+ approval.getGranted());
+ accountLoader.put(info);
+ result.get(label).add(info);
+ }
+ accountLoader.fill();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot get votes", e);
+ }
+ return result;
+ }
+
+ @Override
public void description(String description) throws RestApiException {
DescriptionInput in = new DescriptionInput();
in.description = description;
diff --git a/java/com/google/gerrit/server/api/groups/GroupsImpl.java b/java/com/google/gerrit/server/api/groups/GroupsImpl.java
index e8d6cf4..bae75db 100644
--- a/java/com/google/gerrit/server/api/groups/GroupsImpl.java
+++ b/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -141,7 +141,7 @@
if (req.getUser() != null) {
try {
- list.setUser(accountResolver.parse(req.getUser()).getAccountId());
+ list.setUser(accountResolver.resolve(req.getUser()).asUnique().getAccount().getId());
} catch (Exception e) {
throw asRestApiException("Error looking up user " + req.getUser(), e);
}
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 463c23e..354331e 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -70,7 +70,6 @@
import com.google.gerrit.server.restapi.project.GetParent;
import com.google.gerrit.server.restapi.project.Index;
import com.google.gerrit.server.restapi.project.ListBranches;
-import com.google.gerrit.server.restapi.project.ListChildProjects;
import com.google.gerrit.server.restapi.project.ListDashboards;
import com.google.gerrit.server.restapi.project.ListTags;
import com.google.gerrit.server.restapi.project.ProjectsCollection;
@@ -475,10 +474,17 @@
@Override
public List<ProjectInfo> children(boolean recursive) throws RestApiException {
- ListChildProjects list = children.list();
- list.setRecursive(recursive);
try {
- return list.apply(checkExists());
+ return children.list().withRecursive(recursive).apply(checkExists());
+ } catch (Exception e) {
+ throw asRestApiException("Cannot list children", e);
+ }
+ }
+
+ @Override
+ public List<ProjectInfo> children(int limit) throws RestApiException {
+ try {
+ return children.list().withLimit(limit).apply(checkExists());
} catch (Exception e) {
throw asRestApiException("Cannot list children", e);
}
diff --git a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
index 580ec54..721d878 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -22,7 +22,6 @@
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.restapi.project.ListProjects;
@@ -150,12 +149,12 @@
private List<ProjectInfo> query(QueryRequest r) throws RestApiException {
try {
- QueryProjects myQueryProjects = queryProvider.get();
- myQueryProjects.setQuery(r.getQuery());
- myQueryProjects.setLimit(r.getLimit());
- myQueryProjects.setStart(r.getStart());
-
- return myQueryProjects.apply(TopLevelResource.INSTANCE);
+ return queryProvider
+ .get()
+ .withQuery(r.getQuery())
+ .withLimit(r.getLimit())
+ .withStart(r.getStart())
+ .apply();
} catch (OrmException e) {
throw new RestApiException("Cannot query projects", e);
}
diff --git a/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
index 2b66334..addb7f9 100644
--- a/java/com/google/gerrit/server/args4j/AccountIdHandler.java
+++ b/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.util.cli.Localizable.localizable;
import com.google.gerrit.extensions.client.AuthType;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
@@ -60,10 +61,9 @@
String token = params.getParameter(0);
Account.Id accountId;
try {
- Account a = accountResolver.find(token);
- if (a != null) {
- accountId = a.getId();
- } else {
+ try {
+ accountId = accountResolver.resolve(token).asUnique().getAccount().getId();
+ } catch (UnprocessableEntityException e) {
switch (authType) {
case HTTP_LDAP:
case CLIENT_SSL_CERT_LDAP:
diff --git a/java/com/google/gerrit/server/change/AbandonOp.java b/java/com/google/gerrit/server/change/AbandonOp.java
index 3999955..a43690c 100644
--- a/java/com/google/gerrit/server/change/AbandonOp.java
+++ b/java/com/google/gerrit/server/change/AbandonOp.java
@@ -15,13 +15,9 @@
package com.google.gerrit.server.change;
import com.google.common.base.Strings;
-import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -49,8 +45,6 @@
private final ChangeAbandoned changeAbandoned;
private final String msgTxt;
- private final NotifyHandling notifyHandling;
- private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
private final AccountState accountState;
private Change change;
@@ -59,10 +53,7 @@
public interface Factory {
AbandonOp create(
- @Assisted @Nullable AccountState accountState,
- @Assisted @Nullable String msgTxt,
- @Assisted NotifyHandling notifyHandling,
- @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify);
+ @Assisted @Nullable AccountState accountState, @Assisted @Nullable String msgTxt);
}
@Inject
@@ -72,9 +63,7 @@
PatchSetUtil psUtil,
ChangeAbandoned changeAbandoned,
@Assisted @Nullable AccountState accountState,
- @Assisted @Nullable String msgTxt,
- @Assisted NotifyHandling notifyHandling,
- @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+ @Assisted @Nullable String msgTxt) {
this.abandonedSenderFactory = abandonedSenderFactory;
this.cmUtil = cmUtil;
this.psUtil = psUtil;
@@ -82,8 +71,6 @@
this.accountState = accountState;
this.msgTxt = Strings.nullToEmpty(msgTxt);
- this.notifyHandling = notifyHandling;
- this.accountsToNotify = accountsToNotify;
}
@Nullable
@@ -122,18 +109,18 @@
@Override
public void postUpdate(Context ctx) throws OrmException {
+ NotifyResolver.Result notify = ctx.getNotify(change.getId());
try {
ReplyToChangeSender cm = abandonedSenderFactory.create(ctx.getProject(), change.getId());
if (accountState != null) {
cm.setFrom(accountState.getAccount().getId());
}
cm.setChangeMessage(message.getMessage(), ctx.getWhen());
- cm.setNotify(notifyHandling);
- cm.setAccountsToNotify(accountsToNotify);
+ cm.setNotify(notify);
cm.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
}
- changeAbandoned.fire(change, patchSet, accountState, msgTxt, ctx.getWhen(), notifyHandling);
+ changeAbandoned.fire(change, patchSet, accountState, msgTxt, ctx.getWhen(), notify.handling());
}
}
diff --git a/java/com/google/gerrit/server/change/AddReviewersEmail.java b/java/com/google/gerrit/server/change/AddReviewersEmail.java
index 4173950..d9c5dad 100644
--- a/java/com/google/gerrit/server/change/AddReviewersEmail.java
+++ b/java/com/google/gerrit/server/change/AddReviewersEmail.java
@@ -16,12 +16,8 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
-import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.mail.Address;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
@@ -49,9 +45,7 @@
Collection<Account.Id> copied,
Collection<Address> addedByEmail,
Collection<Address> copiedByEmail,
- NotifyHandling notify,
- ListMultimap<RecipientType, Account.Id> accountsToNotify,
- boolean readyForReview) {
+ NotifyResolver.Result notify) {
// The user knows they added themselves, don't bother emailing them.
Account.Id userId = user.getAccountId();
ImmutableList<Account.Id> toMail =
@@ -64,11 +58,7 @@
try {
AddReviewerSender cm = addReviewerSenderFactory.create(change.getProject(), change.getId());
- // Default to silent operation on WIP changes.
- NotifyHandling defaultNotifyHandling =
- readyForReview ? NotifyHandling.ALL : NotifyHandling.NONE;
- cm.setNotify(MoreObjects.firstNonNull(notify, defaultNotifyHandling));
- cm.setAccountsToNotify(accountsToNotify);
+ cm.setNotify(notify);
cm.setFrom(userId);
cm.addReviewers(toMail);
cm.addReviewersByEmail(addedByEmail);
diff --git a/java/com/google/gerrit/server/change/AddReviewersOp.java b/java/com/google/gerrit/server/change/AddReviewersOp.java
index ea42652..4a97c30 100644
--- a/java/com/google/gerrit/server/change/AddReviewersOp.java
+++ b/java/com/google/gerrit/server/change/AddReviewersOp.java
@@ -25,12 +25,8 @@
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Streams;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.mail.Address;
@@ -69,16 +65,10 @@
* @param accountIds account IDs to add.
* @param addresses email addresses to add.
* @param state resulting reviewer state.
- * @param notify notification handling.
- * @param accountsToNotify additional accounts to notify.
* @return batch update operation.
*/
AddReviewersOp create(
- Set<Account.Id> accountIds,
- Collection<Address> addresses,
- ReviewerState state,
- @Nullable NotifyHandling notify,
- ListMultimap<RecipientType, Account.Id> accountsToNotify);
+ Set<Account.Id> accountIds, Collection<Address> addresses, ReviewerState state);
}
@AutoValue
@@ -118,8 +108,6 @@
private final Set<Account.Id> accountIds;
private final Collection<Address> addresses;
private final ReviewerState state;
- private final NotifyHandling notify;
- private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
// Unlike addedCCs, addedReviewers is a PatchSetApproval because the AddReviewerResult returned
// via the REST API is supposed to include vote information.
@@ -128,6 +116,7 @@
private Collection<Account.Id> addedCCs = ImmutableList.of();
private Collection<Address> addedCCsByEmail = ImmutableList.of();
+ private boolean sendEmail = true;
private Change change;
private PatchSet patchSet;
private Result opResult;
@@ -142,9 +131,7 @@
AddReviewersEmail addReviewersEmail,
@Assisted Set<Account.Id> accountIds,
@Assisted Collection<Address> addresses,
- @Assisted ReviewerState state,
- @Assisted @Nullable NotifyHandling notify,
- @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+ @Assisted ReviewerState state) {
checkArgument(state == REVIEWER || state == CC, "must be %s or %s: %s", REVIEWER, CC, state);
this.approvalsUtil = approvalsUtil;
this.psUtil = psUtil;
@@ -156,8 +143,13 @@
this.accountIds = accountIds;
this.addresses = addresses;
this.state = state;
- this.notify = notify;
- this.accountsToNotify = accountsToNotify;
+ }
+
+ // TODO(dborowitz): This mutable setter is ugly, but a) it's less ugly than adding boolean args
+ // all the way through the constructor stack, and b) this class is slated to be completely
+ // rewritten.
+ public void suppressEmail() {
+ this.sendEmail = false;
}
void setPatchSet(PatchSet patchSet) {
@@ -246,16 +238,16 @@
.setAddedCCs(addedCCs)
.setAddedCCsByEmail(addedCCsByEmail)
.build();
- addReviewersEmail.emailReviewers(
- ctx.getUser().asIdentifiedUser(),
- change,
- Lists.transform(addedReviewers, PatchSetApproval::getAccountId),
- addedCCs,
- addedReviewersByEmail,
- addedCCsByEmail,
- notify,
- accountsToNotify,
- !change.isWorkInProgress());
+ if (sendEmail) {
+ addReviewersEmail.emailReviewers(
+ ctx.getUser().asIdentifiedUser(),
+ change,
+ Lists.transform(addedReviewers, PatchSetApproval::getAccountId),
+ addedCCs,
+ addedReviewersByEmail,
+ addedCCsByEmail,
+ ctx.getNotify(change.getId()));
+ }
if (!addedReviewers.isEmpty()) {
List<AccountState> reviewers =
addedReviewers
diff --git a/java/com/google/gerrit/server/change/BatchAbandon.java b/java/com/google/gerrit/server/change/BatchAbandon.java
index b15db60..8c67531 100644
--- a/java/com/google/gerrit/server/change/BatchAbandon.java
+++ b/java/com/google/gerrit/server/change/BatchAbandon.java
@@ -14,13 +14,8 @@
package com.google.gerrit.server.change;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountState;
@@ -54,14 +49,14 @@
CurrentUser user,
Collection<ChangeData> changes,
String msgTxt,
- NotifyHandling notifyHandling,
- ListMultimap<RecipientType, Account.Id> accountsToNotify)
+ NotifyResolver.Result notify)
throws RestApiException, UpdateException {
if (changes.isEmpty()) {
return;
}
AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null;
try (BatchUpdate u = updateFactory.create(project, user, TimeUtil.nowTs())) {
+ u.setNotify(notify);
for (ChangeData change : changes) {
if (!project.equals(change.project())) {
throw new ResourceConflictException(
@@ -69,9 +64,7 @@
"Project name \"%s\" doesn't match \"%s\"",
change.project().get(), project.get()));
}
- u.addOp(
- change.getId(),
- abandonOpFactory.create(accountState, msgTxt, notifyHandling, accountsToNotify));
+ u.addOp(change.getId(), abandonOpFactory.create(accountState, msgTxt));
}
u.execute();
}
@@ -84,14 +77,7 @@
Collection<ChangeData> changes,
String msgTxt)
throws RestApiException, UpdateException {
- batchAbandon(
- updateFactory,
- project,
- user,
- changes,
- msgTxt,
- NotifyHandling.ALL,
- ImmutableListMultimap.of());
+ batchAbandon(updateFactory, project, user, changes, msgTxt, NotifyResolver.Result.all());
}
public void batchAbandon(
@@ -100,7 +86,6 @@
CurrentUser user,
Collection<ChangeData> changes)
throws RestApiException, UpdateException {
- batchAbandon(
- updateFactory, project, user, changes, "", NotifyHandling.ALL, ImmutableListMultimap.of());
+ batchAbandon(updateFactory, project, user, changes, "", NotifyResolver.Result.all());
}
}
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index fb92ec9..544edcc 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -24,16 +24,13 @@
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
-import com.google.common.collect.ListMultimap;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -123,8 +120,6 @@
private boolean workInProgress;
private List<String> groups = Collections.emptyList();
private boolean validate = true;
- private NotifyHandling notify = NotifyHandling.ALL;
- private ListMultimap<RecipientType, Account.Id> accountsToNotify = ImmutableListMultimap.of();
private Map<String, Short> approvals;
private RequestScopePropagator requestScopePropagator;
private boolean fireRevisionCreated;
@@ -255,17 +250,6 @@
return this;
}
- public ChangeInserter setNotify(NotifyHandling notify) {
- this.notify = notify;
- return this;
- }
-
- public ChangeInserter setAccountsToNotify(
- ListMultimap<RecipientType, Account.Id> accountsToNotify) {
- this.accountsToNotify = requireNonNull(accountsToNotify);
- return this;
- }
-
public ChangeInserter setReviewersAndCcs(
Iterable<Account.Id> reviewers, Iterable<Account.Id> ccs) {
return setReviewersAndCcsAsStrings(
@@ -457,7 +441,8 @@
@Override
public void postUpdate(Context ctx) throws Exception {
reviewerAdditions.postUpdate(ctx);
- if (sendMail && (notify != NotifyHandling.NONE || !accountsToNotify.isEmpty())) {
+ NotifyResolver.Result notify = ctx.getNotify(change.getId());
+ if (sendMail && notify.shouldNotify()) {
Runnable sender =
new Runnable() {
@Override
@@ -468,7 +453,6 @@
cm.setFrom(change.getOwner());
cm.setPatchSet(patchSet, patchSetInfo);
cm.setNotify(notify);
- cm.setAccountsToNotify(accountsToNotify);
cm.addReviewers(
reviewerAdditions
.flattenResults(AddReviewersOp.Result::addedReviewers)
diff --git a/java/com/google/gerrit/server/change/ChangeMessages.java b/java/com/google/gerrit/server/change/ChangeMessages.java
index 41b6855..6cd3726 100644
--- a/java/com/google/gerrit/server/change/ChangeMessages.java
+++ b/java/com/google/gerrit/server/change/ChangeMessages.java
@@ -25,9 +25,7 @@
public String revertChangeDefaultMessage;
public String reviewerCantSeeChange;
- public String reviewerInactive;
public String reviewerInvalid;
- public String reviewerNotFoundUser;
public String reviewerNotFoundUserOrGroup;
public String groupIsNotAllowed;
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 7a553e3..7b911c4 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -31,7 +31,6 @@
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.changes.FixInput;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.common.ProblemInfo;
import com.google.gerrit.extensions.common.ProblemInfo.Status;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -532,12 +531,12 @@
}
}
+ bu.setNotify(NotifyResolver.Result.none());
bu.addOp(
notes.getChangeId(),
inserter
.setValidate(false)
.setFireRevisionCreated(false)
- .setNotify(NotifyHandling.NONE)
.setAllowClosed(true)
.setMessage("Patch set for merged commit inserted by consistency checker"));
bu.addOp(notes.getChangeId(), new FixMergedOp(notFound));
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index 7e063bc..8353501 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -16,12 +16,8 @@
import static com.google.gerrit.server.CommentsUtil.COMMENT_ORDER;
-import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -48,7 +44,6 @@
// on the same set of inputs.
/**
* @param notify setting for handling notification.
- * @param accountsToNotify detailed map of accounts to notify.
* @param notes change notes.
* @param patchSet patch set corresponding to the top-level op
* @param user user the email should come from.
@@ -63,8 +58,7 @@
* @return handle for sending email.
*/
EmailReviewComments create(
- NotifyHandling notify,
- ListMultimap<RecipientType, Account.Id> accountsToNotify,
+ NotifyResolver.Result notify,
ChangeNotes notes,
PatchSet patchSet,
IdentifiedUser user,
@@ -79,8 +73,7 @@
private final CommentSender.Factory commentSenderFactory;
private final ThreadLocalRequestContext requestContext;
- private final NotifyHandling notify;
- private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
+ private final NotifyResolver.Result notify;
private final ChangeNotes notes;
private final PatchSet patchSet;
private final IdentifiedUser user;
@@ -95,8 +88,7 @@
PatchSetInfoFactory patchSetInfoFactory,
CommentSender.Factory commentSenderFactory,
ThreadLocalRequestContext requestContext,
- @Assisted NotifyHandling notify,
- @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify,
+ @Assisted NotifyResolver.Result notify,
@Assisted ChangeNotes notes,
@Assisted PatchSet patchSet,
@Assisted IdentifiedUser user,
@@ -109,7 +101,6 @@
this.commentSenderFactory = commentSenderFactory;
this.requestContext = requestContext;
this.notify = notify;
- this.accountsToNotify = accountsToNotify;
this.notes = notes;
this.patchSet = patchSet;
this.user = user;
@@ -136,7 +127,6 @@
cm.setPatchSetComment(patchSetComment);
cm.setLabels(labels);
cm.setNotify(notify);
- cm.setAccountsToNotify(accountsToNotify);
cm.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email comments for %s", patchSet.getId());
diff --git a/java/com/google/gerrit/server/change/NotifyResolver.java b/java/com/google/gerrit/server/change/NotifyResolver.java
new file mode 100644
index 0000000..65da083
--- /dev/null
+++ b/java/com/google/gerrit/server/change/NotifyResolver.java
@@ -0,0 +1,118 @@
+// 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.change;
+
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
+import com.google.gerrit.extensions.api.changes.NotifyInfo;
+import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class NotifyResolver {
+ @AutoValue
+ public abstract static class Result {
+ public static Result none() {
+ return create(NotifyHandling.NONE);
+ }
+
+ public static Result all() {
+ return create(NotifyHandling.ALL);
+ }
+
+ public static Result create(NotifyHandling notifyHandling) {
+ return create(notifyHandling, ImmutableSetMultimap.of());
+ }
+
+ public static Result create(
+ NotifyHandling handling, ImmutableSetMultimap<RecipientType, Account.Id> recipients) {
+ return new AutoValue_NotifyResolver_Result(handling, recipients);
+ }
+
+ public abstract NotifyHandling handling();
+
+ // TODO(dborowitz): Should be ImmutableSetMultimap.
+ public abstract ImmutableSetMultimap<RecipientType, Account.Id> accounts();
+
+ public Result withHandling(NotifyHandling notifyHandling) {
+ return create(notifyHandling, accounts());
+ }
+
+ public boolean shouldNotify() {
+ return !accounts().isEmpty() || handling().compareTo(NotifyHandling.NONE) > 0;
+ }
+ }
+
+ private final AccountResolver accountResolver;
+
+ @Inject
+ NotifyResolver(AccountResolver accountResolver) {
+ this.accountResolver = accountResolver;
+ }
+
+ public Result resolve(
+ NotifyHandling handling, @Nullable Map<RecipientType, NotifyInfo> notifyDetails)
+ throws BadRequestException, OrmException, IOException, ConfigInvalidException {
+ requireNonNull(handling);
+ ImmutableSetMultimap.Builder<RecipientType, Account.Id> b = ImmutableSetMultimap.builder();
+ if (notifyDetails != null) {
+ for (Map.Entry<RecipientType, NotifyInfo> e : notifyDetails.entrySet()) {
+ b.putAll(e.getKey(), find(e.getValue().accounts));
+ }
+ }
+ return Result.create(handling, b.build());
+ }
+
+ private ImmutableList<Account.Id> find(@Nullable List<String> inputs)
+ throws OrmException, BadRequestException, IOException, ConfigInvalidException {
+ if (inputs == null || inputs.isEmpty()) {
+ return ImmutableList.of();
+ }
+ ImmutableList.Builder<Account.Id> r = ImmutableList.builder();
+ List<String> problems = new ArrayList<>(inputs.size());
+ for (String nameOrEmail : inputs) {
+ try {
+ r.add(accountResolver.resolve(nameOrEmail).asUnique().getAccount().getId());
+ } catch (UnprocessableEntityException e) {
+ problems.add(e.getMessage());
+ }
+ }
+
+ if (!problems.isEmpty()) {
+ throw new BadRequestException(
+ "Some accounts that should be notified could not be resolved: "
+ + problems.stream().collect(joining("\n")));
+ }
+
+ return r.build();
+ }
+}
diff --git a/java/com/google/gerrit/server/change/NotifyUtil.java b/java/com/google/gerrit/server/change/NotifyUtil.java
deleted file mode 100644
index fa6fdfc..0000000
--- a/java/com/google/gerrit/server/change/NotifyUtil.java
+++ /dev/null
@@ -1,116 +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.change;
-
-import static java.util.stream.Collectors.joining;
-
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.NotifyInfo;
-import com.google.gerrit.extensions.api.changes.RecipientType;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.account.AccountResolver;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
-@Singleton
-public class NotifyUtil {
- private final AccountResolver accountResolver;
-
- @Inject
- NotifyUtil(AccountResolver accountResolver) {
- this.accountResolver = accountResolver;
- }
-
- public static boolean shouldNotify(
- NotifyHandling notify, @Nullable Map<RecipientType, NotifyInfo> notifyDetails) {
- if (!isNullOrEmpty(notifyDetails)) {
- return true;
- }
-
- return notify.compareTo(NotifyHandling.NONE) > 0;
- }
-
- private static boolean isNullOrEmpty(@Nullable Map<RecipientType, NotifyInfo> notifyDetails) {
- if (notifyDetails == null || notifyDetails.isEmpty()) {
- return true;
- }
-
- for (NotifyInfo notifyInfo : notifyDetails.values()) {
- if (!isEmpty(notifyInfo)) {
- return false;
- }
- }
-
- return true;
- }
-
- private static boolean isEmpty(NotifyInfo notifyInfo) {
- return notifyInfo.accounts == null || notifyInfo.accounts.isEmpty();
- }
-
- public ListMultimap<RecipientType, Account.Id> resolveAccounts(
- @Nullable Map<RecipientType, NotifyInfo> notifyDetails)
- throws OrmException, BadRequestException, IOException, ConfigInvalidException {
- if (isNullOrEmpty(notifyDetails)) {
- return ImmutableListMultimap.of();
- }
-
- ListMultimap<RecipientType, Account.Id> m = null;
- for (Map.Entry<RecipientType, NotifyInfo> e : notifyDetails.entrySet()) {
- List<String> accounts = e.getValue().accounts;
- if (accounts != null) {
- if (m == null) {
- m = MultimapBuilder.hashKeys().arrayListValues().build();
- }
- m.putAll(e.getKey(), find(accounts));
- }
- }
-
- return m != null ? m : ImmutableListMultimap.of();
- }
-
- private List<Account.Id> find(List<String> nameOrEmails)
- throws OrmException, BadRequestException, IOException, ConfigInvalidException {
- List<String> missing = new ArrayList<>(nameOrEmails.size());
- List<Account.Id> r = new ArrayList<>(nameOrEmails.size());
- for (String nameOrEmail : nameOrEmails) {
- Account a = accountResolver.find(nameOrEmail);
- if (a != null) {
- r.add(a.getId());
- } else {
- missing.add(nameOrEmail);
- }
- }
-
- if (!missing.isEmpty()) {
- throw new BadRequestException(
- "The following accounts that should be notified could not be resolved: "
- + missing.stream().distinct().sorted().collect(joining(", ")));
- }
-
- return r;
- }
-}
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index ec11c1b..f62d943 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -19,14 +19,10 @@
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static java.util.Objects.requireNonNull;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -95,8 +91,6 @@
private boolean checkAddPatchSetPermission = true;
private List<String> groups = Collections.emptyList();
private boolean fireRevisionCreated = true;
- private NotifyHandling notify = NotifyHandling.ALL;
- private ListMultimap<RecipientType, Account.Id> accountsToNotify = ImmutableListMultimap.of();
private boolean allowClosed;
// Fields set during some phase of BatchUpdate.Op.
@@ -170,17 +164,6 @@
return this;
}
- public PatchSetInserter setNotify(NotifyHandling notify) {
- this.notify = requireNonNull(notify);
- return this;
- }
-
- public PatchSetInserter setAccountsToNotify(
- ListMultimap<RecipientType, Account.Id> accountsToNotify) {
- this.accountsToNotify = requireNonNull(accountsToNotify);
- return this;
- }
-
public PatchSetInserter setAllowClosed(boolean allowClosed) {
this.allowClosed = allowClosed;
return this;
@@ -229,7 +212,7 @@
psUtil.insert(
ctx.getRevWalk(), ctx.getUpdate(psId), psId, commitId, newGroups, null, description);
- if (notify != NotifyHandling.NONE) {
+ if (ctx.getNotify(change.getId()).handling() != NotifyHandling.NONE) {
oldReviewers = approvalsUtil.getReviewers(ctx.getNotes());
}
@@ -258,7 +241,8 @@
@Override
public void postUpdate(Context ctx) throws OrmException {
- if (notify != NotifyHandling.NONE || !accountsToNotify.isEmpty()) {
+ NotifyResolver.Result notify = ctx.getNotify(change.getId());
+ if (notify.shouldNotify()) {
try {
ReplacePatchSetSender cm = replacePatchSetFactory.create(ctx.getProject(), change.getId());
cm.setFrom(ctx.getAccountId());
@@ -267,7 +251,6 @@
cm.addReviewers(oldReviewers.byState(REVIEWER));
cm.addExtraCC(oldReviewers.byState(CC));
cm.setNotify(notify);
- cm.setAccountsToNotify(accountsToNotify);
cm.send();
} catch (Exception err) {
logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index a64900b..61bdc76 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -16,7 +16,6 @@
import static com.google.common.base.Preconditions.checkState;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -183,7 +182,6 @@
patchSetInserterFactory
.create(notes, rebasedPatchSetId, rebasedCommit)
.setDescription("Rebase")
- .setNotify(NotifyHandling.NONE)
.setFireRevisionCreated(fireRevisionCreated)
.setCheckAddPatchSetPermission(checkAddPatchSetPermission)
.setValidate(validate);
diff --git a/java/com/google/gerrit/server/change/ReviewerAdder.java b/java/com/google/gerrit/server/change/ReviewerAdder.java
index f318001..6dd0db8 100644
--- a/java/com/google/gerrit/server/change/ReviewerAdder.java
+++ b/java/com/google/gerrit/server/change/ReviewerAdder.java
@@ -24,9 +24,7 @@
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.collect.Streams;
@@ -36,12 +34,10 @@
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AddReviewerResult;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.mail.Address;
@@ -151,7 +147,6 @@
private final AccountLoader.Factory accountLoaderFactory;
private final Config cfg;
private final ReviewerJson json;
- private final NotifyUtil notifyUtil;
private final ProjectCache projectCache;
private final Provider<AnonymousUser> anonymousProvider;
private final AddReviewersOp.Factory addReviewersOpFactory;
@@ -166,7 +161,6 @@
AccountLoader.Factory accountLoaderFactory,
@GerritServerConfig Config cfg,
ReviewerJson json,
- NotifyUtil notifyUtil,
ProjectCache projectCache,
Provider<AnonymousUser> anonymousProvider,
AddReviewersOp.Factory addReviewersOpFactory,
@@ -178,7 +172,6 @@
this.accountLoaderFactory = accountLoaderFactory;
this.cfg = cfg;
this.json = json;
- this.notifyUtil = notifyUtil;
this.projectCache = projectCache;
this.anonymousProvider = anonymousProvider;
this.addReviewersOpFactory = addReviewersOpFactory;
@@ -204,38 +197,39 @@
ChangeNotes notes, CurrentUser user, AddReviewerInput input, boolean allowGroup)
throws OrmException, IOException, PermissionBackendException, ConfigInvalidException {
requireNonNull(input.reviewer);
- ListMultimap<RecipientType, Account.Id> accountsToNotify;
- try {
- accountsToNotify = notifyUtil.resolveAccounts(input.notifyDetails);
- } catch (BadRequestException e) {
- return fail(input, FailureType.OTHER, e.getMessage());
- }
boolean confirmed = input.confirmed();
boolean allowByEmail =
projectCache
.checkedGet(notes.getProjectName())
.is(BooleanProjectConfig.ENABLE_REVIEWER_BY_EMAIL);
- ReviewerAddition byAccountId =
- addByAccountId(input, notes, user, accountsToNotify, allowGroup, allowByEmail);
+ ReviewerAddition byAccountId = addByAccountId(input, notes, user);
ReviewerAddition wholeGroup = null;
- if (byAccountId == null || !byAccountId.exactMatchFound) {
- wholeGroup =
- addWholeGroup(input, notes, user, accountsToNotify, confirmed, allowGroup, allowByEmail);
+ if (!byAccountId.exactMatchFound) {
+ wholeGroup = addWholeGroup(input, notes, user, confirmed, allowGroup, allowByEmail);
if (wholeGroup != null && wholeGroup.exactMatchFound) {
return wholeGroup;
}
}
- if (byAccountId != null) {
+ if (wholeGroup != null
+ && byAccountId.failureType == FailureType.NOT_FOUND
+ && wholeGroup.failureType == FailureType.NOT_FOUND) {
+ return fail(
+ byAccountId.input,
+ FailureType.NOT_FOUND,
+ byAccountId.result.error + "\n" + wholeGroup.result.error);
+ }
+
+ if (byAccountId.failureType != FailureType.NOT_FOUND) {
return byAccountId;
}
if (wholeGroup != null) {
return wholeGroup;
}
- return addByEmail(input, notes, user, accountsToNotify);
+ return addByEmail(input, notes, user);
}
public ReviewerAddition ccCurrentUser(CurrentUser user, RevisionResource revision) {
@@ -245,57 +239,30 @@
revision.getUser(),
ImmutableSet.of(user.getAccountId()),
null,
- ImmutableListMultimap.of(),
true);
}
@Nullable
private ReviewerAddition addByAccountId(
- AddReviewerInput input,
- ChangeNotes notes,
- CurrentUser user,
- ListMultimap<RecipientType, Account.Id> accountsToNotify,
- boolean allowGroup,
- boolean allowByEmail)
+ AddReviewerInput input, ChangeNotes notes, CurrentUser user)
throws OrmException, PermissionBackendException, IOException, ConfigInvalidException {
IdentifiedUser reviewerUser;
boolean exactMatchFound = false;
try {
- reviewerUser = accountResolver.parse(input.reviewer);
+ reviewerUser = accountResolver.resolve(input.reviewer).asUniqueUser();
if (input.reviewer.equalsIgnoreCase(reviewerUser.getName())
|| input.reviewer.equals(String.valueOf(reviewerUser.getAccountId()))) {
exactMatchFound = true;
}
- } catch (UnprocessableEntityException | AuthException e) {
- // AuthException won't occur since the user is authenticated at this point.
- if (!allowGroup && !allowByEmail) {
- // Only return failure if we aren't going to try other interpretations.
- return fail(
- input,
- FailureType.NOT_FOUND,
- MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer));
- }
- return null;
+ } catch (UnprocessableEntityException e) {
+ // Caller might choose to ignore this NOT_FOUND result if they find another result e.g. by
+ // group, but if not, the error message will be useful.
+ return fail(input, FailureType.NOT_FOUND, e.getMessage());
}
if (isValidReviewer(notes.getChange().getDest(), reviewerUser.getAccount())) {
return new ReviewerAddition(
- input,
- notes,
- user,
- ImmutableSet.of(reviewerUser.getAccountId()),
- null,
- accountsToNotify,
- exactMatchFound);
- }
- if (!reviewerUser.getAccount().isActive()) {
- if (allowByEmail && input.state() == CC) {
- return null;
- }
- return fail(
- input,
- FailureType.OTHER,
- MessageFormat.format(ChangeMessages.get().reviewerInactive, input.reviewer));
+ input, notes, user, ImmutableSet.of(reviewerUser.getAccountId()), null, exactMatchFound);
}
return fail(
input,
@@ -308,7 +275,6 @@
AddReviewerInput input,
ChangeNotes notes,
CurrentUser user,
- ListMultimap<RecipientType, Account.Id> accountsToNotify,
boolean confirmed,
boolean allowGroup,
boolean allowByEmail)
@@ -380,15 +346,11 @@
}
}
- return new ReviewerAddition(input, notes, user, reviewers, null, accountsToNotify, true);
+ return new ReviewerAddition(input, notes, user, reviewers, null, true);
}
@Nullable
- private ReviewerAddition addByEmail(
- AddReviewerInput input,
- ChangeNotes notes,
- CurrentUser user,
- ListMultimap<RecipientType, Account.Id> accountsToNotify)
+ private ReviewerAddition addByEmail(AddReviewerInput input, ChangeNotes notes, CurrentUser user)
throws PermissionBackendException {
try {
permissionBackend.user(anonymousProvider.get()).change(notes).check(ChangePermission.READ);
@@ -406,16 +368,11 @@
FailureType.NOT_FOUND,
MessageFormat.format(ChangeMessages.get().reviewerInvalid, input.reviewer));
}
- return new ReviewerAddition(
- input, notes, user, null, ImmutableList.of(adr), accountsToNotify, true);
+ return new ReviewerAddition(input, notes, user, null, ImmutableList.of(adr), true);
}
private boolean isValidReviewer(Branch.NameKey branch, Account member)
throws PermissionBackendException {
- if (!member.isActive()) {
- return false;
- }
-
try {
// Check ref permission instead of change permission, since change permissions take into
// account the private bit, whereas adding a user as a reviewer is explicitly allowing them to
@@ -466,7 +423,6 @@
CurrentUser caller,
@Nullable Iterable<Account.Id> reviewers,
@Nullable Iterable<Address> reviewersByEmail,
- ListMultimap<RecipientType, Account.Id> accountsToNotify,
boolean exactMatchFound) {
checkArgument(
reviewers != null || reviewersByEmail != null,
@@ -481,9 +437,7 @@
this.reviewersByEmail =
reviewersByEmail == null ? ImmutableSet.of() : ImmutableSet.copyOf(reviewersByEmail);
this.caller = caller.asIdentifiedUser();
- op =
- addReviewersOpFactory.create(
- this.reviewers, this.reviewersByEmail, state(), input.notify, accountsToNotify);
+ op = addReviewersOpFactory.create(this.reviewers, this.reviewersByEmail, state());
this.exactMatchFound = exactMatchFound;
}
@@ -568,11 +522,17 @@
Streams.stream(inputs)
.sorted(
comparing(
- i -> i.state(), Ordering.explicit(ReviewerState.CC, ReviewerState.REVIEWER)))
+ AddReviewerInput::state,
+ Ordering.explicit(ReviewerState.CC, ReviewerState.REVIEWER)))
.collect(toImmutableList());
List<ReviewerAddition> additions = new ArrayList<>();
for (AddReviewerInput input : sorted) {
- additions.add(prepare(notes, user, input, allowGroup));
+ ReviewerAddition addition = prepare(notes, user, input, allowGroup);
+ if (addition.op != null) {
+ // Assume any callers preparing a list of batch insertions are handling their own email.
+ addition.op.suppressEmail();
+ }
+ additions.add(addition);
}
return new ReviewerAdditionList(additions);
}
diff --git a/java/com/google/gerrit/server/change/WorkInProgressOp.java b/java/com/google/gerrit/server/change/WorkInProgressOp.java
index 1da6d16..02870fb 100644
--- a/java/com/google/gerrit/server/change/WorkInProgressOp.java
+++ b/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -14,10 +14,8 @@
package com.google.gerrit.server.change;
-import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -92,9 +90,9 @@
private final PatchSetUtil psUtil;
private final boolean workInProgress;
private final Input in;
- private final NotifyHandling notify;
private final WorkInProgressStateChanged stateChanged;
+ private boolean sendEmail = true;
private Change change;
private ChangeNotes notes;
private PatchSet ps;
@@ -114,9 +112,10 @@
this.stateChanged = stateChanged;
this.workInProgress = workInProgress;
this.in = in;
- notify =
- MoreObjects.firstNonNull(
- in.notify, workInProgress ? NotifyHandling.NONE : NotifyHandling.ALL);
+ }
+
+ public void suppressEmail() {
+ this.sendEmail = false;
}
@Override
@@ -160,13 +159,15 @@
@Override
public void postUpdate(Context ctx) {
stateChanged.fire(change, ps, ctx.getAccount(), ctx.getWhen());
- if (workInProgress || notify.ordinal() < NotifyHandling.OWNER_REVIEWERS.ordinal()) {
+ NotifyResolver.Result notify = ctx.getNotify(change.getId());
+ if (workInProgress
+ || notify.handling().compareTo(NotifyHandling.OWNER_REVIEWERS) < 0
+ || !sendEmail) {
return;
}
email
.create(
notify,
- ImmutableListMultimap.of(),
notes,
ps,
ctx.getIdentifiedUser(),
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 1cc1e8f..9650ac2 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -81,7 +81,6 @@
import com.google.gerrit.server.account.AccountDeactivator;
import com.google.gerrit.server.account.AccountExternalIdCreator;
import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.AccountVisibilityProvider;
import com.google.gerrit.server.account.CapabilityCollection;
import com.google.gerrit.server.account.EmailExpander;
@@ -252,8 +251,6 @@
install(new SshAddressesModule());
install(ThreadLocalRequestContext.module());
- bind(AccountResolver.class);
-
factory(AddReviewerSender.Factory.class);
factory(DeleteReviewerSender.Factory.class);
factory(AddKeySender.Factory.class);
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index d6fdf56..898f427 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -16,14 +16,10 @@
import static com.google.common.base.Preconditions.checkArgument;
-import com.google.common.collect.ListMultimap;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.client.ChangeKind;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
-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.reviewdb.client.RefNames;
@@ -32,6 +28,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.ChangeKindCache;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.index.change.ChangeIndexer;
@@ -148,7 +145,6 @@
* @param edit change edit to publish
* @param notify Notify handling that defines to whom email notifications should be sent after the
* change edit is published.
- * @param accountsToNotify Accounts that should be notified after the change edit is published.
* @throws IOException
* @throws OrmException
* @throws UpdateException
@@ -158,9 +154,8 @@
BatchUpdate.Factory updateFactory,
ChangeNotes notes,
CurrentUser user,
- final ChangeEdit edit,
- NotifyHandling notify,
- ListMultimap<RecipientType, Account.Id> accountsToNotify)
+ ChangeEdit edit,
+ NotifyResolver.Result notify)
throws IOException, OrmException, RestApiException, UpdateException {
Change change = edit.getChange();
try (Repository repo = gitManager.openRepository(change.getProject());
@@ -174,11 +169,7 @@
RevCommit squashed = squashEdit(rw, oi, edit.getEditCommit(), basePatchSet);
PatchSet.Id psId = ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
- PatchSetInserter inserter =
- patchSetInserterFactory
- .create(notes, psId, squashed)
- .setNotify(notify)
- .setAccountsToNotify(accountsToNotify);
+ PatchSetInserter inserter = patchSetInserterFactory.create(notes, psId, squashed);
StringBuilder message =
new StringBuilder("Patch Set ").append(inserter.getPatchSetId().get()).append(": ");
@@ -199,6 +190,7 @@
try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.nowTs())) {
bu.setRepository(repo, rw, oi);
+ bu.setNotify(notify);
bu.addOp(change.getId(), inserter.setMessage(message.toString()));
bu.addOp(
change.getId(),
diff --git a/java/com/google/gerrit/server/events/EventBroker.java b/java/com/google/gerrit/server/events/EventBroker.java
index f17a0f0..03b5d54 100644
--- a/java/com/google/gerrit/server/events/EventBroker.java
+++ b/java/com/google/gerrit/server/events/EventBroker.java
@@ -105,7 +105,7 @@
protected void fireEvent(Change change, ChangeEvent event)
throws OrmException, PermissionBackendException {
for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
- CurrentUser user = c.call(l -> l.getUser());
+ CurrentUser user = c.call(UserScopedEventListener::getUser);
if (isVisibleTo(change, user)) {
c.run(l -> l.onEvent(event));
}
@@ -115,7 +115,7 @@
protected void fireEvent(Project.NameKey project, ProjectEvent event) {
for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
- CurrentUser user = c.call(l -> l.getUser());
+ CurrentUser user = c.call(UserScopedEventListener::getUser);
if (isVisibleTo(project, user)) {
c.run(l -> l.onEvent(event));
}
@@ -126,7 +126,7 @@
protected void fireEvent(Branch.NameKey branchName, RefEvent event)
throws PermissionBackendException {
for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
- CurrentUser user = c.call(l -> l.getUser());
+ CurrentUser user = c.call(UserScopedEventListener::getUser);
if (isVisibleTo(branchName, user)) {
c.run(l -> l.onEvent(event));
}
@@ -136,7 +136,7 @@
protected void fireEvent(Event event) throws OrmException, PermissionBackendException {
for (PluginSetEntryContext<UserScopedEventListener> c : listeners) {
- CurrentUser user = c.call(l -> l.getUser());
+ CurrentUser user = c.call(UserScopedEventListener::getUser);
if (isVisibleTo(event, user)) {
c.run(l -> l.onEvent(event));
}
diff --git a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
index e043e9f..3fd69a2 100644
--- a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
+++ b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -24,6 +24,7 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.GpgException;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.patch.PatchListObjectTooLargeException;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -46,7 +47,7 @@
PatchSet patchSet,
AccountState uploader,
Timestamp when,
- NotifyHandling notify) {}
+ NotifyResolver.Result notify) {}
};
private final PluginSetContext<RevisionCreatedListener> listeners;
@@ -68,7 +69,7 @@
PatchSet patchSet,
AccountState uploader,
Timestamp when,
- NotifyHandling notify) {
+ NotifyResolver.Result notify) {
if (listeners.isEmpty()) {
return;
}
@@ -79,7 +80,7 @@
util.revisionInfo(change.getProject(), patchSet),
util.accountInfo(uploader),
when,
- notify);
+ notify.handling());
listeners.runEach(l -> l.onRevisionCreated(event));
} catch (PatchListObjectTooLargeException e) {
logger.atWarning().log("Couldn't fire event: %s", e.getMessage());
diff --git a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
index 88aa2a1..fd4495a 100644
--- a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
+++ b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -45,6 +45,13 @@
import java.util.List;
import java.util.concurrent.ExecutionException;
+/**
+ * Cache based on an index query of the most recent changes. The number of cached items depends on
+ * the index implementation and configuration.
+ *
+ * <p>This cache is intended to be used when filtering references. By design it returns only a
+ * fraction of all changes. These are the changes that were modified last.
+ */
@Singleton
public class SearchingChangeCacheImpl implements GitReferenceUpdatedListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 8e0615c..39b34df 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -49,6 +49,7 @@
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
@@ -93,6 +94,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.SetHashtagsOp;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -682,7 +684,7 @@
// Update superproject gitlinks if required.
if (!branches.isEmpty()) {
try (MergeOpRepoManager orm = ormProvider.get()) {
- orm.setContext(TimeUtil.nowTs(), user);
+ orm.setContext(TimeUtil.nowTs(), user, NotifyResolver.Result.none());
SubmoduleOp op = subOpFactory.create(branches, orm);
op.updateSuperProjects();
} catch (SubmoduleException e) {
@@ -828,9 +830,15 @@
RevWalk rw = new RevWalk(reader)) {
bu.setRepository(repo, rw, ins);
bu.setRefLogMessage("push");
+ if (magicBranch != null) {
+ bu.setNotify(magicBranch.getNotifyForNewChange());
+ }
logger.atFine().log("Adding %d replace requests", newChanges.size());
for (ReplaceRequest replace : replaceByChange.values()) {
+ if (magicBranch != null) {
+ bu.setNotifyHandling(replace.ontoChange, magicBranch.getNotifyHandling(replace.notes));
+ }
replace.addOps(bu, replaceProgress);
}
@@ -1429,7 +1437,7 @@
"Notify handling that defines to whom email notifications "
+ "should be sent. Allowed values are NONE, OWNER, "
+ "OWNER_REVIEWERS, ALL. If not set, the default is ALL.")
- private NotifyHandling notify;
+ private NotifyHandling notifyHandling;
@Option(
name = "--notify-to",
@@ -1559,15 +1567,6 @@
.collect(toImmutableSet());
}
- ListMultimap<RecipientType, Account.Id> getAccountsToNotify() {
- ListMultimap<RecipientType, Account.Id> accountsToNotify =
- MultimapBuilder.hashKeys().arrayListValues().build();
- accountsToNotify.putAll(RecipientType.TO, notifyTo);
- accountsToNotify.putAll(RecipientType.CC, notifyCc);
- accountsToNotify.putAll(RecipientType.BCC, notifyBcc);
- return accountsToNotify;
- }
-
boolean shouldPublishComments() {
if (publishComments) {
return true;
@@ -1627,19 +1626,20 @@
return ref.substring(0, split);
}
- NotifyHandling getNotify() {
- if (notify != null) {
- return notify;
- }
- if (workInProgress) {
- return NotifyHandling.OWNER;
- }
- return NotifyHandling.ALL;
+ NotifyResolver.Result getNotifyForNewChange() {
+ return NotifyResolver.Result.create(
+ firstNonNull(notifyHandling, workInProgress ? NotifyHandling.OWNER : NotifyHandling.ALL),
+ ImmutableSetMultimap.<RecipientType, Account.Id>builder()
+ .putAll(RecipientType.TO, notifyTo)
+ .putAll(RecipientType.CC, notifyCc)
+ .putAll(RecipientType.BCC, notifyBcc)
+ .build());
}
- NotifyHandling getNotify(ChangeNotes notes) {
- if (notify != null) {
- return notify;
+ NotifyHandling getNotifyHandling(ChangeNotes notes) {
+ requireNonNull(notes);
+ if (notifyHandling != null) {
+ return notifyHandling;
}
if (workInProgress || (!ready && notes.getChange().isWorkInProgress())) {
return NotifyHandling.OWNER;
@@ -2483,14 +2483,13 @@
msg.append("\n").append(magicBranch.message);
}
+ bu.setNotify(magicBranch.getNotifyForNewChange());
bu.insertChange(
ins.setReviewersAndCcsAsStrings(
magicBranch.getCombinedReviewers(fromFooters),
magicBranch.getCombinedCcs(fromFooters))
.setApprovals(approvals)
.setMessage(msg.toString())
- .setNotify(magicBranch.getNotify())
- .setAccountsToNotify(magicBranch.getAccountsToNotify())
.setRequestScopePropagator(requestScopePropagator)
.setSendMail(true)
.setPatchSetDescription(magicBranch.message));
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 0f68ba5..84bab4a 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -25,7 +25,6 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
@@ -52,6 +51,7 @@
import com.google.gerrit.server.change.AddReviewersOp;
import com.google.gerrit.server.change.ChangeKindCache;
import com.google.gerrit.server.change.EmailReviewComments;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerAdder;
import com.google.gerrit.server.change.ReviewerAdder.InternalAddReviewerInput;
import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
@@ -512,13 +512,11 @@
}
}
- NotifyHandling notify = magicBranch != null ? magicBranch.getNotify(notes) : NotifyHandling.ALL;
-
+ NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
if (shouldPublishComments()) {
emailCommentsFactory
.create(
notify,
- magicBranch != null ? magicBranch.getAccountsToNotify() : ImmutableListMultimap.of(),
notes,
newPatchSet,
ctx.getUser().asIdentifiedUser(),
@@ -555,10 +553,7 @@
cm.setFrom(ctx.getAccount().getAccount().getId());
cm.setPatchSet(newPatchSet, info);
cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
- if (magicBranch != null) {
- cm.setNotify(magicBranch.getNotify(notes));
- cm.setAccountsToNotify(magicBranch.getAccountsToNotify());
- }
+ cm.setNotify(ctx.getNotify(notes.getChangeId()));
cm.addReviewers(
Streams.concat(
oldRecipients.getReviewers().stream(),
diff --git a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
index acae533..dd5d508 100644
--- a/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
+++ b/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -77,7 +77,8 @@
boolean withException = false;
try {
messages.addAll(
- new DisallowCreationAndDeletionOfUserBranches(perm, allUsersName).onRefOperation(event));
+ new DisallowCreationAndDeletionOfGerritMaintainedBranches(perm, allUsersName)
+ .onRefOperation(event));
refOperationValidationListeners.runEach(
l -> l.onRefOperation(event), ValidationException.class);
} catch (ValidationException e) {
@@ -110,12 +111,12 @@
}
}
- private static class DisallowCreationAndDeletionOfUserBranches
+ private static class DisallowCreationAndDeletionOfGerritMaintainedBranches
implements RefOperationValidationListener {
private final PermissionBackend.WithUser perm;
private final AllUsersName allUsersName;
- DisallowCreationAndDeletionOfUserBranches(
+ DisallowCreationAndDeletionOfGerritMaintainedBranches(
PermissionBackend.WithUser perm, AllUsersName allUsersName) {
this.perm = perm;
this.allUsersName = allUsersName;
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index 66230ea..abc8c90 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -57,13 +57,13 @@
* AccountGroup.UUID)} or {@link #loadForGroupSnapshot(Project.NameKey, Repository,
* AccountGroup.UUID, ObjectId)}.
*
- * <p><strong>Note: </strong>Any modification (group creation or update) only becomes permanent (and
+ * <p><strong>Note:</strong> Any modification (group creation or update) only becomes permanent (and
* hence written to NoteDb) if {@link #commit(MetaDataUpdate)} is called.
*
- * <p><strong>Warning: </strong>This class is a low-level API for groups in NoteDb. Most code which
+ * <p><strong>Warning:</strong> This class is a low-level API for groups in NoteDb. Most code which
* deals with internal Gerrit groups should use {@link Groups} or {@link GroupsUpdate} instead.
*
- * <p><em>Internal details</em>
+ * <h2>Internal details</h2>
*
* <p>Each group is represented by a commit on a branch as defined by {@link
* RefNames#refsGroups(AccountGroup.UUID)}. Previous versions of the group exist as older commits on
@@ -99,7 +99,7 @@
* {@link #setGroupUpdate(InternalGroupUpdate, AuditLogFormatter)} on the returned {@code
* GroupConfig}.
*
- * <p><strong>Note: </strong>The returned {@code GroupConfig} has to be committed via {@link
+ * <p><strong>Note:</strong> The returned {@code GroupConfig} has to be committed via {@link
* #commit(MetaDataUpdate)} in order to create the group for real.
*
* @param projectName the name of the project which holds the NoteDb commits for groups
@@ -216,7 +216,7 @@
* <p>If the group is newly created, the {@code InternalGroupUpdate} can be used to specify
* optional properties.
*
- * <p><strong>Note: </strong>This method doesn't perform the update. It only contains the
+ * <p><strong>Note:</strong> This method doesn't perform the update. It only contains the
* instructions for the update. To apply the update for real and write the result back to NoteDb,
* call {@link #commit(MetaDataUpdate)} on this {@code GroupConfig}.
*
@@ -233,7 +233,7 @@
/**
* Allows the new name of a group to be empty during creation or update.
*
- * <p><strong>Note: </strong>This method exists only to support the migration of legacy groups
+ * <p><strong>Note:</strong> This method exists only to support the migration of legacy groups
* which don't always necessarily have a name. Nowadays, we enforce that groups always have names.
* When we remove the migration code, we can probably remove this method as well.
*/
diff --git a/java/com/google/gerrit/server/group/db/GroupConfigEntry.java b/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
index eff3458..f7a104d 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfigEntry.java
@@ -25,7 +25,7 @@
*
* <p>Each property knows how to read and write its value from/to a JGit {@link Config} file.
*
- * <p><strong>Warning: </strong>This class is a low-level API for properties of groups in NoteDb. It
+ * <p><strong>Warning:</strong> This class is a low-level API for properties of groups in NoteDb. It
* may only be used by {@link GroupConfig}. Other classes should use {@link InternalGroupUpdate} to
* modify the properties of a group.
*/
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index d397b0d..77af248 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -43,9 +43,9 @@
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gwtorm.server.OrmDuplicateKeyException;
-import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Objects;
@@ -75,13 +75,24 @@
* modifications executed by it. For NoteDb, this identity is used as author and committer for
* all related commits.
*
- * <p><strong>Note</strong>: Please use this method with care and rather consider to use the
- * correct annotation on the provider of a {@code GroupsUpdate} instead.
+ * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
+ * com.google.gerrit.server.UserInitiated} annotation on the provider of a {@code GroupsUpdate}
+ * instead.
*
- * @param currentUser the user to which modifications should be attributed, or {@code null} if
- * the Gerrit server identity should be used
+ * @param currentUser the user to which modifications should be attributed
*/
- GroupsUpdate create(@Nullable IdentifiedUser currentUser);
+ GroupsUpdate create(IdentifiedUser currentUser);
+
+ /**
+ * Creates a {@code GroupsUpdate} which uses the server identity to mark database modifications
+ * executed by it. For NoteDb, this identity is used as author and committer for all related
+ * commits.
+ *
+ * <p><strong>Note</strong>: Please use this method with care and consider using the {@link
+ * com.google.gerrit.server.ServerInitiated} annotation on the provider of a {@code
+ * GroupsUpdate} instead.
+ */
+ GroupsUpdate createWithServerIdent();
}
private final GitRepositoryManager repoManager;
@@ -91,14 +102,48 @@
private final Provider<GroupIndexer> indexer;
private final GroupAuditService groupAuditService;
private final RenameGroupOp.Factory renameGroupOpFactory;
- @Nullable private final IdentifiedUser currentUser;
+ private final Optional<IdentifiedUser> currentUser;
private final AuditLogFormatter auditLogFormatter;
private final PersonIdent authorIdent;
private final MetaDataUpdateFactory metaDataUpdateFactory;
private final GitReferenceUpdated gitRefUpdated;
private final RetryHelper retryHelper;
- @Inject
+ @AssistedInject
+ GroupsUpdate(
+ GitRepositoryManager repoManager,
+ AllUsersName allUsersName,
+ GroupBackend groupBackend,
+ GroupCache groupCache,
+ GroupIncludeCache groupIncludeCache,
+ Provider<GroupIndexer> indexer,
+ GroupAuditService auditService,
+ AccountCache accountCache,
+ RenameGroupOp.Factory renameGroupOpFactory,
+ @GerritServerId String serverId,
+ @GerritPersonIdent PersonIdent serverIdent,
+ MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
+ GitReferenceUpdated gitRefUpdated,
+ RetryHelper retryHelper) {
+ this(
+ repoManager,
+ allUsersName,
+ groupBackend,
+ groupCache,
+ groupIncludeCache,
+ indexer,
+ auditService,
+ accountCache,
+ renameGroupOpFactory,
+ serverId,
+ serverIdent,
+ metaDataUpdateInternalFactory,
+ gitRefUpdated,
+ retryHelper,
+ Optional.empty());
+ }
+
+ @AssistedInject
GroupsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
@@ -114,7 +159,41 @@
MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
GitReferenceUpdated gitRefUpdated,
RetryHelper retryHelper,
- @Assisted @Nullable IdentifiedUser currentUser) {
+ @Assisted IdentifiedUser currentUser) {
+ this(
+ repoManager,
+ allUsersName,
+ groupBackend,
+ groupCache,
+ groupIncludeCache,
+ indexer,
+ auditService,
+ accountCache,
+ renameGroupOpFactory,
+ serverId,
+ serverIdent,
+ metaDataUpdateInternalFactory,
+ gitRefUpdated,
+ retryHelper,
+ Optional.of(currentUser));
+ }
+
+ private GroupsUpdate(
+ GitRepositoryManager repoManager,
+ AllUsersName allUsersName,
+ GroupBackend groupBackend,
+ GroupCache groupCache,
+ GroupIncludeCache groupIncludeCache,
+ Provider<GroupIndexer> indexer,
+ GroupAuditService auditService,
+ AccountCache accountCache,
+ RenameGroupOp.Factory renameGroupOpFactory,
+ @GerritServerId String serverId,
+ @GerritPersonIdent PersonIdent serverIdent,
+ MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
+ GitReferenceUpdated gitRefUpdated,
+ RetryHelper retryHelper,
+ Optional<IdentifiedUser> currentUser) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
this.groupCache = groupCache;
@@ -135,7 +214,7 @@
private static MetaDataUpdateFactory getMetaDataUpdateFactory(
MetaDataUpdate.InternalFactory metaDataUpdateInternalFactory,
- @Nullable IdentifiedUser currentUser,
+ Optional<IdentifiedUser> currentUser,
PersonIdent serverIdent,
AuditLogFormatter auditLogFormatter) {
return (projectName, repository, batchRefUpdate) -> {
@@ -143,10 +222,10 @@
metaDataUpdateInternalFactory.create(projectName, repository, batchRefUpdate);
metaDataUpdate.getCommitBuilder().setCommitter(serverIdent);
PersonIdent authorIdent;
- if (currentUser != null) {
- metaDataUpdate.setAuthor(currentUser);
+ if (currentUser.isPresent()) {
+ metaDataUpdate.setAuthor(currentUser.get());
authorIdent =
- auditLogFormatter.getParsableAuthorIdent(currentUser.getAccount(), serverIdent);
+ auditLogFormatter.getParsableAuthorIdent(currentUser.get().getAccount(), serverIdent);
} else {
authorIdent = serverIdent;
}
@@ -156,8 +235,8 @@
}
private static PersonIdent getAuthorIdent(
- PersonIdent serverIdent, @Nullable IdentifiedUser currentUser) {
- return currentUser != null ? createPersonIdent(serverIdent, currentUser) : serverIdent;
+ PersonIdent serverIdent, Optional<IdentifiedUser> currentUser) {
+ return currentUser.map(user -> createPersonIdent(serverIdent, user)).orElse(serverIdent);
}
private static PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
@@ -342,7 +421,7 @@
RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
gitRefUpdated.fire(
- allUsersName, batchRefUpdate, currentUser != null ? currentUser.state() : null);
+ allUsersName, batchRefUpdate, currentUser.map(user -> user.state()).orElse(null));
}
private void updateCachesOnGroupCreation(InternalGroup createdGroup) throws IOException {
@@ -390,20 +469,20 @@
}
private void dispatchAuditEventsOnGroupCreation(InternalGroup createdGroup) {
- if (currentUser == null) {
+ if (!currentUser.isPresent()) {
return;
}
if (!createdGroup.getMembers().isEmpty()) {
groupAuditService.dispatchAddMembers(
- currentUser.getAccountId(),
+ currentUser.get().getAccountId(),
createdGroup.getGroupUUID(),
createdGroup.getMembers(),
createdGroup.getCreatedOn());
}
if (!createdGroup.getSubgroups().isEmpty()) {
groupAuditService.dispatchAddSubgroups(
- currentUser.getAccountId(),
+ currentUser.get().getAccountId(),
createdGroup.getGroupUUID(),
createdGroup.getSubgroups(),
createdGroup.getCreatedOn());
@@ -411,25 +490,34 @@
}
private void dispatchAuditEventsOnGroupUpdate(UpdateResult result, Timestamp updatedOn) {
- if (currentUser == null) {
+ if (!currentUser.isPresent()) {
return;
}
if (!result.getAddedMembers().isEmpty()) {
groupAuditService.dispatchAddMembers(
- currentUser.getAccountId(), result.getGroupUuid(), result.getAddedMembers(), updatedOn);
+ currentUser.get().getAccountId(),
+ result.getGroupUuid(),
+ result.getAddedMembers(),
+ updatedOn);
}
if (!result.getDeletedMembers().isEmpty()) {
groupAuditService.dispatchDeleteMembers(
- currentUser.getAccountId(), result.getGroupUuid(), result.getDeletedMembers(), updatedOn);
+ currentUser.get().getAccountId(),
+ result.getGroupUuid(),
+ result.getDeletedMembers(),
+ updatedOn);
}
if (!result.getAddedSubgroups().isEmpty()) {
groupAuditService.dispatchAddSubgroups(
- currentUser.getAccountId(), result.getGroupUuid(), result.getAddedSubgroups(), updatedOn);
+ currentUser.get().getAccountId(),
+ result.getGroupUuid(),
+ result.getAddedSubgroups(),
+ updatedOn);
}
if (!result.getDeletedSubgroups().isEmpty()) {
groupAuditService.dispatchDeleteSubgroups(
- currentUser.getAccountId(),
+ currentUser.get().getAccountId(),
result.getGroupUuid(),
result.getDeletedSubgroups(),
updatedOn);
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 52dac9d..593fb85 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -24,6 +24,7 @@
import static com.google.gerrit.index.FieldDef.storedOnly;
import static com.google.gerrit.index.FieldDef.timestamp;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
@@ -191,6 +192,29 @@
exact(ChangeQueryBuilder.FIELD_EXTENSION).buildRepeatable(ChangeField::getExtensions);
public static Set<String> getExtensions(ChangeData cd) throws OrmException {
+ return extensions(cd).collect(toSet());
+ }
+
+ /**
+ * File extensions of each file modified in the current patch set as a sorted list. The purpose of
+ * this field is to allow matching changes that only touch files with certain file extensions.
+ */
+ public static final FieldDef<ChangeData, String> ONLY_EXTENSIONS =
+ exact(ChangeQueryBuilder.FIELD_ONLY_EXTENSIONS).build(ChangeField::getAllExtensionsAsList);
+
+ public static String getAllExtensionsAsList(ChangeData cd) throws OrmException {
+ return extensions(cd).distinct().sorted().collect(joining(","));
+ }
+
+ /**
+ * Returns a stream with all file extensions that are used by files in the given change. A file
+ * extension is defined as the portion of the filename following the final `.`. Files with no `.`
+ * in their name have no extension. For them an empty string is returned as part of the stream.
+ *
+ * <p>If the change contains multiple files with the same extension the extension is returned
+ * multiple times in the stream (once per file).
+ */
+ private static Stream<String> extensions(ChangeData cd) throws OrmException {
try {
return cd.currentFilePaths()
.stream()
@@ -198,14 +222,69 @@
// If we want to find "all Java files", we want to match both .java and .JAVA, even if we
// normally care about case sensitivity. (Whether we should change the existing file/path
// predicates to be case insensitive is a separate question.)
- .map(f -> Files.getFileExtension(f).toLowerCase(Locale.US))
- .filter(e -> !e.isEmpty())
+ .map(f -> Files.getFileExtension(f).toLowerCase(Locale.US));
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+ }
+
+ /** Footers from the commit message of the current patch set. */
+ public static final FieldDef<ChangeData, Iterable<String>> FOOTER =
+ exact(ChangeQueryBuilder.FIELD_FOOTER).buildRepeatable(ChangeField::getFooters);
+
+ public static Set<String> getFooters(ChangeData cd) throws OrmException {
+ try {
+ return cd.commitFooters()
+ .stream()
+ .map(f -> f.toString().toLowerCase(Locale.US))
.collect(toSet());
} catch (IOException e) {
throw new OrmException(e);
}
}
+ /** Folders that are touched by the current patch set. */
+ public static final FieldDef<ChangeData, Iterable<String>> DIRECTORY =
+ exact(ChangeQueryBuilder.FIELD_DIRECTORY).buildRepeatable(ChangeField::getDirectories);
+
+ public static Set<String> getDirectories(ChangeData cd) throws OrmException {
+ List<String> paths;
+ try {
+ paths = cd.currentFilePaths();
+ } catch (IOException e) {
+ throw new OrmException(e);
+ }
+
+ Splitter s = Splitter.on('/').omitEmptyStrings();
+ Set<String> r = new HashSet<>();
+ for (String path : paths) {
+ StringBuilder directory = new StringBuilder();
+ directory.append("");
+ r.add(directory.toString());
+ String nextPart = null;
+ for (String part : s.split(path)) {
+ if (nextPart != null) {
+ r.add(nextPart);
+
+ if (directory.length() > 0) {
+ directory.append("/");
+ }
+ directory.append(nextPart);
+
+ String intermediateDir = directory.toString();
+ int i = intermediateDir.indexOf('/');
+ while (i >= 0) {
+ r.add(intermediateDir);
+ intermediateDir = intermediateDir.substring(i + 1);
+ i = intermediateDir.indexOf('/');
+ }
+ }
+ nextPart = part;
+ }
+ }
+ return r;
+ }
+
/** Owner/creator of the change. */
public static final FieldDef<ChangeData, Integer> OWNER =
integer(ChangeQueryBuilder.FIELD_OWNER).build(changeGetter(c -> c.getOwner().get()));
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index cd24c92..cde6a64 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -103,7 +103,16 @@
@Deprecated static final Schema<ChangeData> V51 = schema(V50, ChangeField.TOTAL_COMMENT_COUNT);
- static final Schema<ChangeData> V52 = schema(V51, ChangeField.EXTENSION);
+ @Deprecated static final Schema<ChangeData> V52 = schema(V51, ChangeField.EXTENSION);
+
+ @Deprecated static final Schema<ChangeData> V53 = schema(V52, ChangeField.ONLY_EXTENSIONS);
+
+ @Deprecated static final Schema<ChangeData> V54 = schema(V53, ChangeField.FOOTER);
+
+ @Deprecated static final Schema<ChangeData> V55 = schema(V54, ChangeField.DIRECTORY);
+
+ // The computation of the 'extension' field is changed, hence reindexing is required.
+ static final Schema<ChangeData> V56 = schema(V55);
public static final String NAME = "changes";
public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
diff --git a/java/com/google/gerrit/server/mail/MailUtil.java b/java/com/google/gerrit/server/mail/MailUtil.java
index 185e7f0..3a5a2cf 100644
--- a/java/com/google/gerrit/server/mail/MailUtil.java
+++ b/java/com/google/gerrit/server/mail/MailUtil.java
@@ -18,7 +18,7 @@
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import com.google.gerrit.common.FooterConstants;
-import com.google.gerrit.common.errors.NoSuchAccountException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.account.AccountResolver;
@@ -29,6 +29,7 @@
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.FooterLine;
@@ -36,7 +37,7 @@
public static MailRecipients getRecipientsFromFooters(
AccountResolver accountResolver, List<FooterLine> footerLines)
- throws OrmException, IOException {
+ throws OrmException, IOException, ConfigInvalidException {
MailRecipients recipients = new MailRecipients();
for (FooterLine footerLine : footerLines) {
try {
@@ -45,7 +46,7 @@
} else if (footerLine.matches(FooterKey.CC)) {
recipients.cc.add(toAccountId(accountResolver, footerLine.getValue().trim()));
}
- } catch (NoSuchAccountException e) {
+ } catch (UnprocessableEntityException e) {
continue;
}
}
@@ -59,13 +60,10 @@
return recipients;
}
+ @SuppressWarnings("deprecation")
private static Account.Id toAccountId(AccountResolver accountResolver, String nameOrEmail)
- throws OrmException, NoSuchAccountException, IOException {
- Account a = accountResolver.findByNameOrEmail(nameOrEmail);
- if (a == null) {
- throw new NoSuchAccountException("\"" + nameOrEmail + "\" is not registered");
- }
- return a.getId();
+ throws OrmException, UnprocessableEntityException, IOException, ConfigInvalidException {
+ return accountResolver.resolveByNameOrEmail(nameOrEmail).asUnique().getAccount().getId();
}
private static boolean isReviewer(FooterLine candidateFooterLine) {
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index a1d745e..e087325 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -17,10 +17,8 @@
import static java.util.stream.Collectors.toList;
import com.google.common.base.Strings;
-import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.Extension;
@@ -315,8 +313,7 @@
// Send email notifications
outgoingMailFactory
.create(
- NotifyHandling.ALL,
- ArrayListMultimap.create(),
+ ctx.getNotify(notes.getChangeId()),
notes,
patchSet,
ctx.getUser().asIdentifiedUser(),
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 62d629a..22923c0 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -183,7 +183,7 @@
setChangeUrlHeader();
setCommitIdHeader();
- if (notify.ordinal() >= NotifyHandling.OWNER_REVIEWERS.ordinal()) {
+ if (notify.handling().compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) {
try {
addByEmail(
RecipientType.CC, changeData.reviewersByEmail().byState(ReviewerStateInternal.CC));
@@ -320,7 +320,7 @@
/** BCC any user who has starred this change. */
protected void bccStarredBy() {
- if (!NotifyHandling.ALL.equals(notify)) {
+ if (!NotifyHandling.ALL.equals(notify.handling())) {
return;
}
@@ -342,7 +342,7 @@
@Override
protected final Watchers getWatchers(NotifyType type, boolean includeWatchersFromNotifyConfig)
throws OrmException {
- if (!NotifyHandling.ALL.equals(notify)) {
+ if (!NotifyHandling.ALL.equals(notify.handling())) {
return new Watchers();
}
@@ -352,7 +352,8 @@
/** Any user who has published comments on this change. */
protected void ccAllApprovals() {
- if (!NotifyHandling.ALL.equals(notify) && !NotifyHandling.OWNER_REVIEWERS.equals(notify)) {
+ if (!NotifyHandling.ALL.equals(notify.handling())
+ && !NotifyHandling.OWNER_REVIEWERS.equals(notify.handling())) {
return;
}
@@ -367,7 +368,8 @@
/** Users who have non-zero approval codes on the change. */
protected void ccExistingReviewers() {
- if (!NotifyHandling.ALL.equals(notify) && !NotifyHandling.OWNER_REVIEWERS.equals(notify)) {
+ if (!NotifyHandling.ALL.equals(notify.handling())
+ && !NotifyHandling.OWNER_REVIEWERS.equals(notify.handling())) {
return;
}
@@ -404,7 +406,7 @@
protected Set<Account.Id> getAuthors() {
Set<Account.Id> authors = new HashSet<>();
- switch (notify) {
+ switch (notify.handling()) {
case NONE:
break;
case ALL:
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index ec28bcb..e2a7d92 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -153,10 +153,10 @@
protected void init() throws EmailException {
super.init();
- if (notify.compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) {
+ if (notify.handling().compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) {
ccAllApprovals();
}
- if (notify.compareTo(NotifyHandling.ALL) >= 0) {
+ if (notify.handling().compareTo(NotifyHandling.ALL) >= 0) {
bccStarredBy();
includeWatchers(NotifyType.ALL_COMMENTS, !change.isWorkInProgress() && !change.isPrivate());
}
diff --git a/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index f94f1ca..c45da40 100644
--- a/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -59,7 +59,7 @@
setHeader("Message-ID", getChangeMessageThreadId());
- switch (notify) {
+ switch (notify.handling()) {
case NONE:
case OWNER:
break;
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 043eee9..9c5f977 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -18,11 +18,8 @@
import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED;
import static java.util.Objects.requireNonNull;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.errors.EmailException;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailFormat;
@@ -33,6 +30,7 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.UserIdentity;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.validators.OutgoingEmailValidationListener;
import com.google.gerrit.server.validators.ValidationException;
@@ -65,13 +63,12 @@
private Address smtpFromAddress;
private StringBuilder textBody;
private StringBuilder htmlBody;
- private ListMultimap<RecipientType, Account.Id> accountsToNotify = ImmutableListMultimap.of();
protected Map<String, Object> soyContext;
protected Map<String, Object> soyContextEmailData;
protected List<String> footers;
protected final EmailArguments args;
protected Account.Id fromId;
- protected NotifyHandling notify = NotifyHandling.ALL;
+ protected NotifyResolver.Result notify = NotifyResolver.Result.all();
protected OutgoingEmail(EmailArguments ea, String mc) {
args = ea;
@@ -83,21 +80,17 @@
fromId = id;
}
- public void setNotify(NotifyHandling notify) {
+ public void setNotify(NotifyResolver.Result notify) {
this.notify = requireNonNull(notify);
}
- public void setAccountsToNotify(ListMultimap<RecipientType, Account.Id> accountsToNotify) {
- this.accountsToNotify = requireNonNull(accountsToNotify);
- }
-
/**
* Format and enqueue the message for delivery.
*
* @throws EmailException
*/
public void send() throws EmailException {
- if (NotifyHandling.NONE.equals(notify) && accountsToNotify.isEmpty()) {
+ if (!notify.shouldNotify()) {
return;
}
@@ -129,7 +122,7 @@
// on their behalf to others.
//
add(RecipientType.CC, fromId);
- } else if (!accountsToNotify.containsValue(fromId) && rcptTo.remove(fromId)) {
+ } else if (!notify.accounts().containsValue(fromId) && rcptTo.remove(fromId)) {
// If they don't want a copy, but we queued one up anyway,
// drop them from the recipient lists.
//
@@ -238,8 +231,8 @@
setHeader(FieldName.MESSAGE_ID, "");
setHeader(MailHeader.AUTO_SUBMITTED.fieldName(), "auto-generated");
- for (RecipientType recipientType : accountsToNotify.keySet()) {
- add(recipientType, accountsToNotify.get(recipientType));
+ for (RecipientType recipientType : notify.accounts().keySet()) {
+ add(recipientType, notify.accounts().get(recipientType));
}
setHeader(MailHeader.MESSAGE_TYPE.fieldName(), messageClass);
@@ -412,7 +405,7 @@
return false;
}
- if ((accountsToNotify == null || accountsToNotify.isEmpty())
+ if (notify.accounts().isEmpty()
&& smtpRcptTo.size() == 1
&& rcptTo.size() == 1
&& rcptTo.contains(fromId)) {
diff --git a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
index 2398b82..f2844c4 100644
--- a/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
+++ b/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
@@ -63,7 +63,8 @@
//
reviewers.remove(fromId);
}
- if (notify == NotifyHandling.ALL || notify == NotifyHandling.OWNER_REVIEWERS) {
+ if (notify.handling() == NotifyHandling.ALL
+ || notify.handling() == NotifyHandling.OWNER_REVIEWERS) {
add(RecipientType.TO, reviewers);
add(RecipientType.CC, extraCC);
}
diff --git a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java b/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
index 507b45c..3ea4923 100644
--- a/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
+++ b/java/com/google/gerrit/server/notedb/CommentJsonMigrator.java
@@ -98,7 +98,7 @@
}
progress.refsUpdated =
- bru.getCommands().stream().map(c -> c.getRefName()).collect(toImmutableList());
+ bru.getCommands().stream().map(ReceiveCommand::getRefName).collect(toImmutableList());
if (!bru.getCommands().isEmpty()) {
if (!dryRun) {
ins.flush();
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index b5c03ce..c189f33 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -14,16 +14,20 @@
package com.google.gerrit.server.permissions;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CACHE_AUTOMERGE;
-import static com.google.gerrit.reviewdb.client.RefNames.REFS_CHANGES;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS_SELF;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toMap;
+import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
@@ -123,21 +127,70 @@
.setRate());
}
+ /** Filters given refs and tags by visibility. */
Map<String, Ref> filter(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
throws PermissionBackendException {
+ // See if we can get away with a single, cheap ref evaluation.
+ if (refs.size() == 1) {
+ String refName = Iterables.getOnlyElement(refs.values()).getName();
+ if (opts.filterMeta() && isMetadata(refName)) {
+ return ImmutableMap.of();
+ }
+ if (RefNames.isRefsChanges(refName)) {
+ return canSeeSingleChangeRef(refName) ? refs : ImmutableMap.of();
+ }
+ }
+
+ // Perform an initial ref filtering with all the refs the caller asked for. If we find tags that
+ // we have to investigate (deferred tags) separately then perform a reachability check starting
+ // from all visible branches (refs/heads/*).
+ Result initialRefFilter = filterRefs(refs, repo, opts);
+ Map<String, Ref> visibleRefs = initialRefFilter.visibleRefs();
+ if (!initialRefFilter.deferredTags().isEmpty()) {
+ Result allVisibleBranches = filterRefs(getTaggableRefsMap(repo), repo, opts);
+ checkState(
+ allVisibleBranches.deferredTags().isEmpty(),
+ "unexpected tags found when filtering refs/heads/* " + allVisibleBranches.deferredTags());
+
+ TagMatcher tags =
+ tagCache
+ .get(projectState.getNameKey())
+ .matcher(tagCache, repo, allVisibleBranches.visibleRefs().values());
+ for (Ref tag : initialRefFilter.deferredTags()) {
+ try {
+ if (tags.isReachable(tag)) {
+ visibleRefs.put(tag.getName(), tag);
+ }
+ } catch (IOException e) {
+ throw new PermissionBackendException(e);
+ }
+ }
+ }
+ return visibleRefs;
+ }
+
+ /**
+ * Filters refs by visibility. Returns tags where visibility can't be trivially computed
+ * separately for later ref-walk-based visibility computation. Tags where visibility is trivial to
+ * compute will be returned as part of {@link Result#visibleRefs()}.
+ */
+ Result filterRefs(Map<String, Ref> refs, Repository repo, RefFilterOptions opts)
+ throws PermissionBackendException {
if (projectState.isAllUsers()) {
refs = addUsersSelfSymref(refs);
}
+ // TODO(hiesel): Remove when optimization is done.
boolean hasReadOnRefsStar =
checkProjectPermission(permissionBackendForProject, ProjectPermission.READ);
if (skipFullRefEvaluationIfAllRefsAreVisible && !projectState.isAllUsers()) {
if (projectState.statePermitsRead() && hasReadOnRefsStar) {
skipFilterCount.increment();
- return refs;
+ return new AutoValue_DefaultRefFilter_Result(refs, ImmutableList.of());
} else if (projectControl.allRefsAreVisible(ImmutableSet.of(RefNames.REFS_CONFIG))) {
skipFilterCount.increment();
- return fastHideRefsMetaConfig(refs);
+ return new AutoValue_DefaultRefFilter_Result(
+ fastHideRefsMetaConfig(refs), ImmutableList.of());
}
}
fullFilterCount.increment();
@@ -161,7 +214,6 @@
Map<String, Ref> result = new HashMap<>();
List<Ref> deferredTags = new ArrayList<>();
-
for (Ref ref : refs.values()) {
String name = ref.getName();
Change.Id changeId;
@@ -236,41 +288,15 @@
}
}
}
-
- // 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() || opts.filterTagsSeparately())) {
- TagMatcher tags =
- tagCache
- .get(projectState.getNameKey())
- .matcher(
- tagCache,
- repo,
- opts.filterTagsSeparately()
- ? filter(
- getTaggableRefsMap(repo),
- repo,
- opts.toBuilder().setFilterTagsSeparately(false).build())
- .values()
- : result.values());
- for (Ref tag : deferredTags) {
- try {
- if (tags.isReachable(tag)) {
- result.put(tag.getName(), tag);
- }
- } catch (IOException e) {
- throw new PermissionBackendException(e);
- }
- }
- }
-
- return result;
+ return new AutoValue_DefaultRefFilter_Result(result, deferredTags);
}
/**
* Returns all refs tag we regard as starting points for reachability computation for tags. In
- * general, these are all refs not managed by Gerrit.
+ * general, these are all refs not managed by Gerrit excluding symbolic refs and tags.
+ *
+ * <p>We exclude symbolic refs because their target will be included and this will suffice for
+ * computing reachability.
*/
private static Map<String, Ref> getTaggableRefsMap(Repository repo)
throws PermissionBackendException {
@@ -280,7 +306,9 @@
.stream()
.filter(
r ->
- !RefNames.isGerritRef(r.getName()) && !r.getName().startsWith(RefNames.REFS_TAGS))
+ !RefNames.isGerritRef(r.getName())
+ && !r.getName().startsWith(RefNames.REFS_TAGS)
+ && !r.isSymbolic())
.collect(toMap(Ref::getName, r -> r));
} catch (IOException e) {
throw new PermissionBackendException(e);
@@ -418,7 +446,7 @@
}
private boolean isMetadata(String name) {
- return name.startsWith(REFS_CHANGES) || RefNames.isRefsEdit(name);
+ return RefNames.isRefsChanges(name) || RefNames.isRefsEdit(name);
}
private static boolean isTag(Ref ref) {
@@ -457,4 +485,46 @@
return isAdmin
|| (user != null && user.getEffectiveGroups().contains(group.getOwnerGroupUUID()));
}
+
+ /**
+ * Returns true if the user can see the provided change ref. Uses NoteDb for evaluation, hence
+ * does not suffer from the limitations documented in {@link SearchingChangeCacheImpl}.
+ *
+ * <p>This code lets users fetch changes that are not among the fraction of most recently modified
+ * changes that {@link SearchingChangeCacheImpl} returns. This works only when Git Protocol v2
+ * with refs-in-wants is used as that enables Gerrit to skip traditional advertisement of all
+ * visible refs.
+ */
+ private boolean canSeeSingleChangeRef(String refName) throws PermissionBackendException {
+ // We are treating just a single change ref. We are therefore not going through regular ref
+ // filtering, but use NoteDb directly. This makes it so that we can always serve this ref
+ // even if the change is not part of the set of most recent changes that
+ // SearchingChangeCacheImpl returns.
+ Change.Id cId = Change.Id.fromRef(refName);
+ checkNotNull(cId, "invalid change id for ref %s", refName);
+ ChangeNotes notes;
+ try {
+ notes = changeNotesFactory.create(projectState.getNameKey(), cId);
+ } catch (OrmException e) {
+ throw new PermissionBackendException("can't construct change notes", e);
+ }
+ try {
+ permissionBackendForProject.change(notes).check(ChangePermission.READ);
+ return true;
+ } catch (AuthException e) {
+ return false;
+ }
+ }
+
+ @AutoValue
+ abstract static class Result {
+ /** Subset of the refs passed into the computation that is visible to the user. */
+ abstract Map<String, Ref> visibleRefs();
+
+ /**
+ * List of tags where we couldn't figure out visibility in the first pass and need to do an
+ * expensive ref walk.
+ */
+ abstract List<Ref> deferredTags();
+ }
}
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index a87eb24..80fb35b 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -358,9 +358,6 @@
/** 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();
-
/**
* Select only refs with names matching prefixes per {@link
* org.eclipse.jgit.lib.RefDatabase#getRefsByPrefix}.
@@ -372,7 +369,6 @@
public static Builder builder() {
return new AutoValue_PermissionBackend_RefFilterOptions.Builder()
.setFilterMeta(false)
- .setFilterTagsSeparately(false)
.setPrefixes(Collections.singletonList(""));
}
@@ -380,8 +376,6 @@
public abstract static class Builder {
public abstract Builder setFilterMeta(boolean val);
- public abstract Builder setFilterTagsSeparately(boolean val);
-
public abstract Builder setPrefixes(List<String> prefixes);
public abstract RefFilterOptions build();
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 60d9ec6..9fdc7e8 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -50,6 +50,7 @@
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.UsedAt;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.ProjectWatches.NotifyType;
import com.google.gerrit.server.config.AllProjectsName;
@@ -1242,7 +1243,7 @@
for (NotifyConfig nc : sort(notifySections.values())) {
nc.getGroups()
.stream()
- .map(gr -> gr.getUUID())
+ .map(GroupReference::getUUID)
.filter(Objects::nonNull)
.forEach(keepGroups::add);
List<String> email =
@@ -1542,6 +1543,7 @@
return m.stream().sorted().collect(toImmutableList());
}
+ @UsedAt(UsedAt.Project.GOOGLE)
public boolean hasLegacyPermissions() {
return hasLegacyPermissions;
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 45847c8..93ece2b 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -16,6 +16,7 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.reviewdb.client.Change.CHANGE_ID_PATTERN;
+import static com.google.gerrit.server.account.AccountResolver.isSelf;
import static com.google.gerrit.server.query.change.ChangeData.asChanges;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
@@ -54,6 +55,7 @@
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.account.GroupMembers;
@@ -137,8 +139,11 @@
public static final String FIELD_COMMENTBY = "commentby";
public static final String FIELD_COMMIT = "commit";
public static final String FIELD_COMMITTER = "committer";
+ public static final String FIELD_DIRECTORY = "directory";
public static final String FIELD_EXACTCOMMITTER = "exactcommitter";
public static final String FIELD_EXTENSION = "extension";
+ public static final String FIELD_ONLY_EXTENSIONS = "onlyextensions";
+ public static final String FIELD_FOOTER = "footer";
public static final String FIELD_CONFLICTS = "conflicts";
public static final String FIELD_DELETED = "deleted";
public static final String FIELD_DELTA = "delta";
@@ -748,6 +753,45 @@
}
@Operator
+ public Predicate<ChangeData> onlyexts(String extList) throws QueryParseException {
+ return onlyextensions(extList);
+ }
+
+ @Operator
+ public Predicate<ChangeData> onlyextensions(String extList) throws QueryParseException {
+ if (args.getSchema().hasField(ChangeField.ONLY_EXTENSIONS)) {
+ return new FileExtensionListPredicate(extList);
+ }
+ throw new QueryParseException(
+ "'onlyextensions' operator is not supported by change index version");
+ }
+
+ @Operator
+ public Predicate<ChangeData> footer(String footer) throws QueryParseException {
+ if (args.getSchema().hasField(ChangeField.FOOTER)) {
+ return new FooterPredicate(footer);
+ }
+ throw new QueryParseException("'footer' operator is not supported by change index version");
+ }
+
+ @Operator
+ public Predicate<ChangeData> dir(String directory) throws QueryParseException {
+ return directory(directory);
+ }
+
+ @Operator
+ public Predicate<ChangeData> directory(String directory) throws QueryParseException {
+ if (args.getSchema().hasField(ChangeField.DIRECTORY)) {
+ if (directory.startsWith("^")) {
+ return new RegexDirectoryPredicate(directory);
+ }
+
+ return new DirectoryPredicate(directory);
+ }
+ throw new QueryParseException("'directory' operator is not supported by change index version");
+ }
+
+ @Operator
public Predicate<ChangeData> label(String name)
throws QueryParseException, OrmException, IOException, ConfigInvalidException {
Set<Account.Id> accounts = null;
@@ -905,23 +949,25 @@
return new HasDraftByPredicate(who);
}
- private boolean isSelf(String who) {
- return "self".equals(who) || "me".equals(who);
- }
-
@Operator
public Predicate<ChangeData> visibleto(String who)
throws QueryParseException, OrmException, IOException, ConfigInvalidException {
if (isSelf(who)) {
return is_visible();
}
- Set<Account.Id> m = args.accountResolver.findAll(who);
- if (!m.isEmpty()) {
+ try {
return Predicate.or(
- m.stream().map(id -> visibleto(args.userFactory.create(id))).collect(toImmutableList()));
+ parseAccount(who)
+ .stream()
+ .map(a -> visibleto(args.userFactory.create(a)))
+ .collect(toImmutableList()));
+ } catch (QueryParseException e) {
+ if (e instanceof QueryRequiresAuthException) {
+ throw e;
+ }
+ // Otherwise continue: if it's not an account, maybe it's a group?
}
- // If its not an account, maybe its a group?
Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
if (!suggestions.isEmpty()) {
HashSet<AccountGroup.UUID> ids = new HashSet<>();
@@ -1295,14 +1341,14 @@
private Set<Account.Id> parseAccount(String who)
throws QueryParseException, OrmException, IOException, ConfigInvalidException {
- if (isSelf(who)) {
- return Collections.singleton(self());
+ try {
+ return args.accountResolver.resolve(who).asNonEmptyIdSet();
+ } catch (UnresolvableAccountException e) {
+ if (e.isSelf()) {
+ throw new QueryRequiresAuthException(e.getMessage(), e);
+ }
+ throw new QueryParseException(e.getMessage(), e);
}
- Set<Account.Id> matches = args.accountResolver.findAll(who);
- if (matches.isEmpty()) {
- throw error("User " + who + " not found");
- }
- return matches;
}
private GroupReference parseGroup(String group) throws QueryParseException {
diff --git a/java/com/google/gerrit/server/query/change/DirectoryPredicate.java b/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
new file mode 100644
index 0000000..676a208
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.common.base.CharMatcher;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+import java.util.Locale;
+
+public class DirectoryPredicate extends ChangeIndexPredicate {
+ private static String clean(String directory) {
+ return CharMatcher.is('/').trimFrom(directory).toLowerCase(Locale.US);
+ }
+
+ DirectoryPredicate(String value) {
+ super(ChangeField.DIRECTORY, clean(value));
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return ChangeField.getDirectories(cd).contains(value);
+ }
+
+ @Override
+ public int getCost() {
+ return 0;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
new file mode 100644
index 0000000..3399338
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.base.Splitter;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+
+public class FileExtensionListPredicate extends ChangeIndexPredicate {
+ private static String clean(String extList) {
+ return Splitter.on(',')
+ .splitToList(extList)
+ .stream()
+ .map(FileExtensionPredicate::clean)
+ .distinct()
+ .sorted()
+ .collect(joining(","));
+ }
+
+ FileExtensionListPredicate(String value) {
+ super(ChangeField.ONLY_EXTENSIONS, clean(value));
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return ChangeField.getAllExtensionsAsList(cd).equals(value);
+ }
+
+ @Override
+ public int getCost() {
+ return 0;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
index ee5030a..5353f11 100644
--- a/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
@@ -19,7 +19,7 @@
import java.util.Locale;
public class FileExtensionPredicate extends ChangeIndexPredicate {
- private static String clean(String ext) {
+ static String clean(String ext) {
if (ext.startsWith(".")) {
ext = ext.substring(1);
}
diff --git a/java/com/google/gerrit/server/query/change/FooterPredicate.java b/java/com/google/gerrit/server/query/change/FooterPredicate.java
new file mode 100644
index 0000000..1d7d19b
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FooterPredicate.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+import java.util.Locale;
+
+public class FooterPredicate extends ChangeIndexPredicate {
+ private static String clean(String value) {
+ int indexEquals = value.indexOf('=');
+ int indexColon = value.indexOf(':');
+
+ // footer key cannot contain '='
+ if (indexEquals > 0 && (indexEquals < indexColon || indexColon < 0)) {
+ value = value.substring(0, indexEquals) + ": " + value.substring(indexEquals + 1);
+ }
+ return value.toLowerCase(Locale.US);
+ }
+
+ FooterPredicate(String value) {
+ super(ChangeField.FOOTER, clean(value));
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return ChangeField.getFooters(cd).contains(value);
+ }
+
+ @Override
+ public int getCost() {
+ return 0;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java b/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java
new file mode 100644
index 0000000..1d49f1e
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/RegexDirectoryPredicate.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gwtorm.server.OrmException;
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+public class RegexDirectoryPredicate extends ChangeRegexPredicate {
+ protected final RunAutomaton pattern;
+
+ public RegexDirectoryPredicate(String re) {
+ super(ChangeField.DIRECTORY, re);
+
+ if (re.startsWith("^")) {
+ re = re.substring(1);
+ }
+
+ if (re.endsWith("$") && !re.endsWith("\\$")) {
+ re = re.substring(0, re.length() - 1);
+ }
+
+ this.pattern = new RunAutomaton(new RegExp(re).toAutomaton());
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return ChangeField.getDirectories(cd).stream().anyMatch(pattern::run);
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
index 296dc17..d5dc692 100644
--- a/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/group/GroupQueryBuilder.java
@@ -24,9 +24,11 @@
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryBuilder;
import com.google.gerrit.index.query.QueryParseException;
+import com.google.gerrit.index.query.QueryRequiresAuthException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.account.GroupCache;
@@ -157,11 +159,14 @@
private Set<Account.Id> parseAccount(String nameOrEmail)
throws QueryParseException, OrmException, IOException, ConfigInvalidException {
- Set<Account.Id> foundAccounts = args.accountResolver.findAll(nameOrEmail);
- if (foundAccounts.isEmpty()) {
- throw error("User " + nameOrEmail + " not found");
+ try {
+ return args.accountResolver.resolve(nameOrEmail).asNonEmptyIdSet();
+ } catch (UnresolvableAccountException e) {
+ if (e.isSelf()) {
+ throw new QueryRequiresAuthException(e.getMessage(), e);
+ }
+ throw new QueryParseException(e.getMessage(), e);
}
- return foundAccounts;
}
private AccountGroup.UUID parseGroup(String groupNameOrUuid) throws QueryParseException {
diff --git a/java/com/google/gerrit/server/query/project/ProjectPredicates.java b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
index 2e406aa..5f13236 100644
--- a/java/com/google/gerrit/server/query/project/ProjectPredicates.java
+++ b/java/com/google/gerrit/server/query/project/ProjectPredicates.java
@@ -27,6 +27,10 @@
return new ProjectPredicate(ProjectField.NAME, nameKey.get());
}
+ public static Predicate<ProjectData> parent(Project.NameKey parentNameKey) {
+ return new ProjectPredicate(ProjectField.PARENT_NAME, parentNameKey.get());
+ }
+
public static Predicate<ProjectData> inname(String name) {
return new ProjectPredicate(ProjectField.NAME_PART, name.toLowerCase(Locale.US));
}
diff --git a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
index 872d3e0..4923015 100644
--- a/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/project/ProjectQueryBuilder.java
@@ -45,6 +45,11 @@
}
@Operator
+ public Predicate<ProjectData> parent(String parentName) {
+ return ProjectPredicates.parent(new Project.NameKey(parentName));
+ }
+
+ @Operator
public Predicate<ProjectData> inname(String namePart) {
if (namePart.isEmpty()) {
return name(namePart);
diff --git a/java/com/google/gerrit/server/restapi/account/AccountsCollection.java b/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
index 61d71d8..35922f4 100644
--- a/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
+++ b/java/com/google/gerrit/server/restapi/account/AccountsCollection.java
@@ -21,9 +21,8 @@
import com.google.gerrit.extensions.restapi.RestCollection;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
import com.google.gerrit.server.account.AccountResource;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -35,18 +34,15 @@
@Singleton
public class AccountsCollection implements RestCollection<TopLevelResource, AccountResource> {
private final AccountResolver accountResolver;
- private final AccountControl.Factory accountControlFactory;
private final Provider<QueryAccounts> list;
private final DynamicMap<RestView<AccountResource>> views;
@Inject
public AccountsCollection(
AccountResolver accountResolver,
- AccountControl.Factory accountControlFactory,
Provider<QueryAccounts> list,
DynamicMap<RestView<AccountResource>> views) {
this.accountResolver = accountResolver;
- this.accountControlFactory = accountControlFactory;
this.list = list;
this.views = views;
}
@@ -55,12 +51,14 @@
public AccountResource parse(TopLevelResource root, IdString id)
throws ResourceNotFoundException, AuthException, OrmException, IOException,
ConfigInvalidException {
- IdentifiedUser user = accountResolver.parseId(id.get());
- if (user == null || !accountControlFactory.get().canSee(user.getAccount().getId())) {
- throw new ResourceNotFoundException(
- String.format("Account '%s' is not found or ambiguous", id));
+ try {
+ return new AccountResource(accountResolver.resolve(id.get()).asUniqueUser());
+ } catch (UnresolvableAccountException e) {
+ if (e.isSelf()) {
+ throw new AuthException(e.getMessage(), e);
+ }
+ throw new ResourceNotFoundException(e.getMessage(), e);
}
- return new AccountResource(user);
}
@Override
diff --git a/java/com/google/gerrit/server/restapi/account/Module.java b/java/com/google/gerrit/server/restapi/account/Module.java
index 9b012f7..f41764d 100644
--- a/java/com/google/gerrit/server/restapi/account/Module.java
+++ b/java/com/google/gerrit/server/restapi/account/Module.java
@@ -118,7 +118,7 @@
@ServerInitiated
AccountsUpdate provideServerInitiatedAccountsUpdate(
AccountsUpdate.Factory accountsUpdateFactory, ExternalIdNotes.Factory extIdNotesFactory) {
- return accountsUpdateFactory.create(null, extIdNotesFactory);
+ return accountsUpdateFactory.createWithServerIdent(extIdNotesFactory);
}
@Provides
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index 851752d..05de9e4 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -14,16 +14,12 @@
package com.google.gerrit.server.restapi.change;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.api.changes.AbandonInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.webui.UiAction;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PatchSetUtil;
@@ -31,7 +27,7 @@
import com.google.gerrit.server.change.AbandonOp;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -53,7 +49,7 @@
private final ChangeJson.Factory json;
private final AbandonOp.Factory abandonOpFactory;
- private final NotifyUtil notifyUtil;
+ private final NotifyResolver notifyResolver;
private final PatchSetUtil patchSetUtil;
@Inject
@@ -61,12 +57,12 @@
ChangeJson.Factory json,
RetryHelper retryHelper,
AbandonOp.Factory abandonOpFactory,
- NotifyUtil notifyUtil,
+ NotifyResolver notifyResolver,
PatchSetUtil patchSetUtil) {
super(retryHelper);
this.json = json;
this.abandonOpFactory = abandonOpFactory;
- this.notifyUtil = notifyUtil;
+ this.notifyResolver = notifyResolver;
this.patchSetUtil = patchSetUtil;
}
@@ -87,8 +83,7 @@
rsrc.getNotes(),
rsrc.getUser(),
input.message,
- notify,
- notifyUtil.resolveAccounts(input.notifyDetails));
+ notifyResolver.resolve(notify, input.notifyDetails));
return json.noOptions().format(change);
}
@@ -103,8 +98,7 @@
notes,
user,
"",
- defaultNotify(notes.getChange()),
- ImmutableListMultimap.of());
+ NotifyResolver.Result.create(defaultNotify(notes.getChange())));
}
public Change abandon(
@@ -115,8 +109,7 @@
notes,
user,
msgTxt,
- defaultNotify(notes.getChange()),
- ImmutableListMultimap.of());
+ NotifyResolver.Result.create(defaultNotify(notes.getChange())));
}
public Change abandon(
@@ -124,12 +117,12 @@
ChangeNotes notes,
CurrentUser user,
String msgTxt,
- NotifyHandling notifyHandling,
- ListMultimap<RecipientType, Account.Id> accountsToNotify)
+ NotifyResolver.Result notify)
throws RestApiException, UpdateException {
AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null;
- AbandonOp op = abandonOpFactory.create(accountState, msgTxt, notifyHandling, accountsToNotify);
+ AbandonOp op = abandonOpFactory.create(accountState, msgTxt);
try (BatchUpdate u = updateFactory.create(notes.getProjectName(), user, TimeUtil.nowTs())) {
+ u.setNotify(notify);
u.addOp(notes.getChangeId(), op).execute();
}
return op.getChange();
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 5317efc..da3f936 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -14,12 +14,15 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.common.base.MoreObjects.firstNonNull;
+
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -37,7 +40,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.change.ChangeInserter;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
@@ -103,7 +106,7 @@
private final ChangeNotes.Factory changeNotesFactory;
private final ProjectCache projectCache;
private final ApprovalsUtil approvalsUtil;
- private final NotifyUtil notifyUtil;
+ private final NotifyResolver notifyResolver;
@Inject
CherryPickChange(
@@ -118,7 +121,7 @@
ChangeNotes.Factory changeNotesFactory,
ProjectCache projectCache,
ApprovalsUtil approvalsUtil,
- NotifyUtil notifyUtil) {
+ NotifyResolver notifyResolver) {
this.seq = seq;
this.queryProvider = queryProvider;
this.gitManager = gitManager;
@@ -130,7 +133,7 @@
this.changeNotesFactory = changeNotesFactory;
this.projectCache = projectCache;
this.approvalsUtil = approvalsUtil;
- this.notifyUtil = notifyUtil;
+ this.notifyResolver = notifyResolver;
}
public Result cherryPick(
@@ -246,11 +249,12 @@
}
try (BatchUpdate bu = batchUpdateFactory.create(project, identifiedUser, now)) {
bu.setRepository(git, revWalk, oi);
+ bu.setNotify(resolveNotify(input));
Change.Id changeId;
if (destChanges.size() == 1) {
// The change key exists on the destination branch. The cherry pick
// will be added as a new patch set.
- changeId = insertPatchSet(bu, git, destChanges.get(0).notes(), cherryPickCommit, input);
+ changeId = insertPatchSet(bu, git, destChanges.get(0).notes(), cherryPickCommit);
} else {
// Change key not found on destination branch. We can create a new
// change.
@@ -315,19 +319,12 @@
}
private Change.Id insertPatchSet(
- BatchUpdate bu,
- Repository git,
- ChangeNotes destNotes,
- CodeReviewCommit cherryPickCommit,
- CherryPickInput input)
- throws IOException, OrmException, BadRequestException, ConfigInvalidException {
+ BatchUpdate bu, Repository git, ChangeNotes destNotes, CodeReviewCommit cherryPickCommit)
+ throws IOException {
Change destChange = destNotes.getChange();
PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId());
PatchSetInserter inserter = patchSetInserterFactory.create(destNotes, psId, cherryPickCommit);
- inserter
- .setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".")
- .setNotify(input.notify)
- .setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+ inserter.setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".");
bu.addOp(destChange.getId(), inserter);
return destChange.getId();
}
@@ -340,7 +337,7 @@
@Nullable Change sourceChange,
ObjectId sourceCommit,
CherryPickInput input)
- throws OrmException, IOException, BadRequestException, ConfigInvalidException {
+ throws OrmException, IOException {
Change.Id changeId = new Change.Id(seq.nextChangeId());
ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName);
Branch.NameKey sourceBranch = sourceChange == null ? null : sourceChange.getDest();
@@ -350,9 +347,7 @@
.setTopic(topic)
.setWorkInProgress(
(sourceChange != null && sourceChange.isWorkInProgress())
- || !cherryPickCommit.getFilesWithGitConflicts().isEmpty())
- .setNotify(input.notify)
- .setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+ || !cherryPickCommit.getFilesWithGitConflicts().isEmpty());
if (input.keepReviewers && sourceChange != null) {
ReviewerSet reviewerSet =
approvalsUtil.getReviewers(changeNotesFactory.createChecked(sourceChange));
@@ -368,6 +363,12 @@
return changeId;
}
+ private NotifyResolver.Result resolveNotify(CherryPickInput input)
+ throws BadRequestException, OrmException, ConfigInvalidException, IOException {
+ return notifyResolver.resolve(
+ firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
+ }
+
private String messageForDestinationChange(
PatchSet.Id patchSetId,
Branch.NameKey sourceBranch,
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index c712e31..9395d9d 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -14,14 +14,15 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.common.base.MoreObjects.firstNonNull;
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;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.client.ChangeStatus;
-import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
@@ -44,12 +45,11 @@
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -76,7 +76,6 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.List;
@@ -114,7 +113,7 @@
private final PatchSetUtil psUtil;
private final MergeUtil.Factory mergeUtilFactory;
private final SubmitType submitType;
- private final NotifyUtil notifyUtil;
+ private final NotifyResolver notifyResolver;
private final ContributorAgreementsChecker contributorAgreements;
private final boolean disablePrivateChanges;
@@ -135,7 +134,7 @@
PatchSetUtil psUtil,
@GerritServerConfig Config config,
MergeUtil.Factory mergeUtilFactory,
- NotifyUtil notifyUtil,
+ NotifyResolver notifyResolver,
ContributorAgreementsChecker contributorAgreements) {
super(retryHelper);
this.anonymousCowardName = anonymousCowardName;
@@ -153,7 +152,7 @@
this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
this.disablePrivateChanges = config.getBoolean("change", null, "disablePrivateChanges", false);
this.mergeUtilFactory = mergeUtilFactory;
- this.notifyUtil = notifyUtil;
+ this.notifyResolver = notifyResolver;
this.contributorAgreements = contributorAgreements;
}
@@ -162,6 +161,35 @@
BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input)
throws OrmException, IOException, InvalidChangeOperationException, RestApiException,
UpdateException, PermissionBackendException, ConfigInvalidException {
+ IdentifiedUser me = user.get().asIdentifiedUser();
+ checkAndSanitizeChangeInput(input, me);
+
+ ProjectResource projectResource = projectsCollection.parse(input.project);
+ ProjectState projectState = projectResource.getProjectState();
+ projectState.checkStatePermitsWrite();
+
+ Project.NameKey project = projectResource.getNameKey();
+ contributorAgreements.check(project, user.get());
+
+ checkRequiredPermissions(project, input.branch);
+
+ Change newChange = createNewChange(input, me, projectState, updateFactory);
+ ChangeJson json = jsonFactory.noOptions();
+ return Response.created(json.format(newChange));
+ }
+
+ /**
+ * Checks and sanitizes the user input, e.g. check whether the input is legal; clean the input so
+ * that it meets the requirement for creating a change; set a field based on the global configs,
+ * etc.
+ *
+ * @param input the {@code ChangeInput} from the request. Note this method modify the {@code
+ * ChangeInput} object so that it can be reused directly by follow-up code.
+ * @param me the user who sent the current request to create a change.
+ * @throws BadRequestException if the input is not legal.
+ */
+ private void checkAndSanitizeChangeInput(ChangeInput input, IdentifiedUser me)
+ throws RestApiException, PermissionBackendException, IOException {
if (Strings.isNullOrEmpty(input.project)) {
throw new BadRequestException("project must be non-empty");
}
@@ -169,34 +197,58 @@
if (Strings.isNullOrEmpty(input.branch)) {
throw new BadRequestException("branch must be non-empty");
}
+ input.branch = RefNames.fullName(input.branch);
- String subject = clean(Strings.nullToEmpty(input.subject));
- if (Strings.isNullOrEmpty(subject)) {
+ String subject = Strings.nullToEmpty(input.subject);
+ subject = subject.replaceAll("(?m)^#.*$\n?", "").trim();
+ if (subject.isEmpty()) {
throw new BadRequestException("commit message must be non-empty");
}
+ input.subject = subject;
- if (input.status != null) {
- if (input.status != ChangeStatus.NEW) {
- throw new BadRequestException("unsupported change status");
- }
+ if (input.topic != null) {
+ input.topic = Strings.emptyToNull(input.topic.trim());
+ }
+
+ if (input.status != null && input.status != ChangeStatus.NEW) {
+ throw new BadRequestException("unsupported change status");
}
if (input.baseChange != null && input.baseCommit != null) {
throw new BadRequestException("only provide one of base_change or base_commit");
}
- ProjectResource rsrc = projectsCollection.parse(input.project);
- boolean privateByDefault = rsrc.getProjectState().is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
+ ProjectResource projectResource = projectsCollection.parse(input.project);
+ // Checks whether the change to be created should be a private change.
+ boolean privateByDefault =
+ projectResource.getProjectState().is(BooleanProjectConfig.PRIVATE_BY_DEFAULT);
boolean isPrivate = input.isPrivate == null ? privateByDefault : input.isPrivate;
-
if (isPrivate && disablePrivateChanges) {
throw new MethodNotAllowedException("private changes are disabled");
}
+ input.isPrivate = isPrivate;
- contributorAgreements.check(rsrc.getNameKey(), rsrc.getUser());
+ ProjectState projectState = projectResource.getProjectState();
- Project.NameKey project = rsrc.getNameKey();
- String refName = RefNames.fullName(input.branch);
+ if (input.workInProgress == null) {
+ if (projectState.is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)) {
+ input.workInProgress = true;
+ } else {
+ input.workInProgress =
+ firstNonNull(me.state().getGeneralPreferences().workInProgressByDefault, false);
+ }
+ }
+
+ if (input.merge != null) {
+ if (!(submitType.equals(SubmitType.MERGE_ALWAYS)
+ || submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
+ throw new BadRequestException("Submit type: " + submitType + " is not supported");
+ }
+ }
+ }
+
+ private void checkRequiredPermissions(Project.NameKey project, String refName)
+ throws ResourceNotFoundException, AuthException, PermissionBackendException {
try {
permissionBackend.currentUser().project(project).ref(refName).check(RefPermission.READ);
} catch (AuthException e) {
@@ -208,132 +260,159 @@
.project(project)
.ref(refName)
.check(RefPermission.CREATE_CHANGE);
- rsrc.getProjectState().checkStatePermitsWrite();
+ }
- try (Repository git = gitManager.openRepository(project);
+ private Change createNewChange(
+ ChangeInput input,
+ IdentifiedUser me,
+ ProjectState projectState,
+ BatchUpdate.Factory updateFactory)
+ throws RestApiException, OrmException, PermissionBackendException, IOException,
+ ConfigInvalidException, UpdateException {
+ try (Repository git = gitManager.openRepository(projectState.getNameKey());
ObjectInserter oi = git.newObjectInserter();
ObjectReader reader = oi.newReader();
RevWalk rw = new RevWalk(reader)) {
- ObjectId parentCommit;
- List<String> groups;
- Ref destRef = git.getRefDatabase().exactRef(refName);
+ PatchSet basePatchSet = null;
+ List<String> groups = Collections.emptyList();
if (input.baseChange != null) {
- List<ChangeNotes> notes = changeFinder.find(input.baseChange);
- if (notes.size() != 1) {
- throw new UnprocessableEntityException("Base change not found: " + input.baseChange);
- }
- ChangeNotes change = Iterables.getOnlyElement(notes);
- try {
- permissionBackend.currentUser().change(change).check(ChangePermission.READ);
- } catch (AuthException e) {
- throw new UnprocessableEntityException("Read not permitted for " + input.baseChange);
- }
- PatchSet ps = psUtil.current(change);
- parentCommit = ObjectId.fromString(ps.getRevision().get());
- groups = ps.getGroups();
- } else if (input.baseCommit != null) {
- try {
- parentCommit = ObjectId.fromString(input.baseCommit);
- } catch (InvalidObjectIdException e) {
- throw new UnprocessableEntityException(
- String.format("Base %s doesn't represent a valid SHA-1", input.baseCommit));
- }
- RevCommit parentRevCommit = rw.parseCommit(parentCommit);
- RevCommit destRefRevCommit = rw.parseCommit(destRef.getObjectId());
- if (!rw.isMergedInto(parentRevCommit, destRefRevCommit)) {
- throw new BadRequestException(
- String.format("Commit %s doesn't exist on ref %s", input.baseCommit, refName));
- }
- groups = Collections.emptyList();
- } else {
- if (destRef != null) {
- if (Boolean.TRUE.equals(input.newBranch)) {
- throw new ResourceConflictException(
- String.format("Branch %s already exists.", refName));
- }
- parentCommit = destRef.getObjectId();
- } else {
- if (Boolean.TRUE.equals(input.newBranch)) {
- parentCommit = null;
- } else {
- throw new BadRequestException("Must provide a destination branch");
- }
- }
- groups = Collections.emptyList();
+ ChangeNotes baseChange = getBaseChange(input.baseChange);
+ basePatchSet = psUtil.current(baseChange);
+ groups = basePatchSet.getGroups();
}
+ ObjectId parentCommit =
+ getParentCommit(git, rw, input.branch, input.newBranch, basePatchSet, input.baseCommit);
+
RevCommit mergeTip = parentCommit == null ? null : rw.parseCommit(parentCommit);
Timestamp now = TimeUtil.nowTs();
- IdentifiedUser me = user.get().asIdentifiedUser();
PersonIdent author = me.newCommitterIdent(now, serverTimeZone);
- AccountState accountState = me.state();
- GeneralPreferencesInfo info = accountState.getGeneralPreferences();
-
- boolean isWorkInProgress =
- input.workInProgress == null
- ? rsrc.getProjectState().is(BooleanProjectConfig.WORK_IN_PROGRESS_BY_DEFAULT)
- || MoreObjects.firstNonNull(info.workInProgressByDefault, false)
- : input.workInProgress;
-
- // Add a Change-Id line if there isn't already one
- String commitMessage = 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 =
- Joiner.on("\n")
- .join(
- commitMessage.trim(),
- String.format(
- "%s%s",
- SIGNED_OFF_BY_TAG,
- accountState.getAccount().getNameEmail(anonymousCowardName)));
- }
+ String commitMessage = getCommitMessage(input.subject, me, oi, mergeTip, author);
RevCommit c;
if (input.merge != null) {
// create a merge commit
- if (!(submitType.equals(SubmitType.MERGE_ALWAYS)
- || submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
- throw new BadRequestException("Submit type: " + submitType + " is not supported");
- }
- c =
- newMergeCommit(
- git, oi, rw, rsrc.getProjectState(), mergeTip, input.merge, author, commitMessage);
+ c = newMergeCommit(git, oi, rw, projectState, mergeTip, input.merge, author, commitMessage);
} else {
// create an empty commit
c = newCommit(oi, rw, author, mergeTip, commitMessage);
}
Change.Id changeId = new Change.Id(seq.nextChangeId());
- ChangeInserter ins = changeInserterFactory.create(changeId, c, refName);
+ ChangeInserter ins = changeInserterFactory.create(changeId, c, input.branch);
ins.setMessage(String.format("Uploaded patch set %s.", ins.getPatchSetId().get()));
- String topic = input.topic;
- if (topic != null) {
- topic = Strings.emptyToNull(topic.trim());
- }
- ins.setTopic(topic);
- ins.setPrivate(isPrivate);
- ins.setWorkInProgress(isWorkInProgress);
+ ins.setTopic(input.topic);
+ ins.setPrivate(input.isPrivate);
+ ins.setWorkInProgress(input.workInProgress);
ins.setGroups(groups);
- ins.setNotify(input.notify);
- ins.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
- try (BatchUpdate bu = updateFactory.create(project, me, now)) {
+ try (BatchUpdate bu = updateFactory.create(projectState.getNameKey(), me, now)) {
bu.setRepository(git, rw, oi);
+ bu.setNotify(
+ notifyResolver.resolve(
+ firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
bu.insertChange(ins);
bu.execute();
}
- ChangeJson json = jsonFactory.noOptions();
- return Response.created(json.format(ins.getChange()));
+ return ins.getChange();
} catch (IllegalArgumentException e) {
throw new BadRequestException(e.getMessage());
}
}
+ private ChangeNotes getBaseChange(String baseChange)
+ throws OrmException, UnprocessableEntityException, PermissionBackendException {
+ List<ChangeNotes> notes = changeFinder.find(baseChange);
+ if (notes.size() != 1) {
+ throw new UnprocessableEntityException("Base change not found: " + baseChange);
+ }
+ ChangeNotes change = Iterables.getOnlyElement(notes);
+ try {
+ permissionBackend.currentUser().change(change).check(ChangePermission.READ);
+ } catch (AuthException e) {
+ throw new UnprocessableEntityException("Read not permitted for " + baseChange);
+ }
+
+ return change;
+ }
+
+ @Nullable
+ private ObjectId getParentCommit(
+ Repository repo,
+ RevWalk revWalk,
+ String inputBranch,
+ @Nullable Boolean newBranch,
+ @Nullable PatchSet basePatchSet,
+ @Nullable String baseCommit)
+ throws BadRequestException, IOException, UnprocessableEntityException,
+ ResourceConflictException {
+ if (basePatchSet != null) {
+ return ObjectId.fromString(basePatchSet.getRevision().get());
+ }
+
+ Ref destRef = repo.getRefDatabase().exactRef(inputBranch);
+ ObjectId parentCommit;
+ if (baseCommit != null) {
+ try {
+ parentCommit = ObjectId.fromString(baseCommit);
+ } catch (InvalidObjectIdException e) {
+ throw new UnprocessableEntityException(
+ String.format("Base %s doesn't represent a valid SHA-1", baseCommit));
+ }
+
+ RevCommit parentRevCommit = revWalk.parseCommit(parentCommit);
+ RevCommit destRefRevCommit = revWalk.parseCommit(destRef.getObjectId());
+ if (!revWalk.isMergedInto(parentRevCommit, destRefRevCommit)) {
+ throw new BadRequestException(
+ String.format("Commit %s doesn't exist on ref %s", baseCommit, inputBranch));
+ }
+ } else {
+ if (destRef != null) {
+ if (Boolean.TRUE.equals(newBranch)) {
+ throw new ResourceConflictException(
+ String.format("Branch %s already exists.", inputBranch));
+ }
+ parentCommit = destRef.getObjectId();
+ } else {
+ if (Boolean.TRUE.equals(newBranch)) {
+ parentCommit = null;
+ } else {
+ throw new BadRequestException("Must provide a destination branch");
+ }
+ }
+ }
+
+ return parentCommit;
+ }
+
+ private String getCommitMessage(
+ String subject,
+ IdentifiedUser me,
+ ObjectInserter objectInserter,
+ RevCommit mergeTip,
+ PersonIdent author)
+ throws IOException {
+ // Add a Change-Id line if there isn't already one
+ String commitMessage = subject;
+ if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
+ ObjectId treeId = mergeTip == null ? emptyTreeId(objectInserter) : mergeTip.getTree();
+ ObjectId id = ChangeIdUtil.computeChangeId(treeId, mergeTip, author, author, commitMessage);
+ commitMessage = ChangeIdUtil.insertId(commitMessage, id);
+ }
+
+ if (Boolean.TRUE.equals(me.state().getGeneralPreferences().signedOffBy)) {
+ commitMessage =
+ Joiner.on("\n")
+ .join(
+ commitMessage.trim(),
+ String.format(
+ "%s%s",
+ SIGNED_OFF_BY_TAG,
+ me.state().getAccount().getNameEmail(anonymousCowardName)));
+ }
+
+ return commitMessage;
+ }
+
private static RevCommit newCommit(
ObjectInserter oi,
RevWalk rw,
@@ -376,8 +455,7 @@
MergeUtil mergeUtil = mergeUtilFactory.create(projectState);
// default merge strategy from project settings
String mergeStrategy =
- MoreObjects.firstNonNull(
- Strings.emptyToNull(merge.strategy), mergeUtil.mergeStrategyName());
+ firstNonNull(Strings.emptyToNull(merge.strategy), mergeUtil.mergeStrategyName());
return MergeUtil.createMergeCommit(
oi,
@@ -390,8 +468,7 @@
rw);
}
- private static ObjectId insert(ObjectInserter inserter, CommitBuilder commit)
- throws IOException, UnsupportedEncodingException {
+ private static ObjectId insert(ObjectInserter inserter, CommitBuilder commit) throws IOException {
ObjectId id = inserter.insert(commit);
inserter.flush();
return id;
@@ -400,16 +477,4 @@
private static ObjectId emptyTreeId(ObjectInserter inserter) throws IOException {
return inserter.insert(new TreeFormatter());
}
-
- /**
- * Remove comment lines from a commit message.
- *
- * <p>Based on {@link org.eclipse.jgit.util.ChangeIdUtil#clean}.
- *
- * @param msg
- * @return message without comment lines, possibly empty.
- */
- private String clean(String msg) {
- return msg.replaceAll("(?m)^#.*$\n?", "").trim();
- }
}
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index 6efe959..6df490c 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -17,7 +17,6 @@
import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.MergeInput;
@@ -41,6 +40,7 @@
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeUtil;
@@ -183,11 +183,10 @@
patchSetInserterFactory.create(rsrc.getNotes(), nextPsId, newCommit);
try (BatchUpdate bu = updateFactory.create(project, me, now)) {
bu.setRepository(git, rw, oi);
+ bu.setNotify(NotifyResolver.Result.none());
psInserter
.setMessage("Uploaded patch set " + nextPsId.get() + ".")
- .setNotify(NotifyHandling.NONE)
- .setCheckAddPatchSetPermission(false)
- .setNotify(NotifyHandling.NONE);
+ .setCheckAddPatchSetPermission(false);
if (groups != null) {
psInserter.setGroups(groups);
}
diff --git a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
index 571c319..092f118 100644
--- a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.conditions.BooleanCondition;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -54,7 +55,7 @@
@Override
protected Response<String> applyImpl(
- BatchUpdate.Factory updateFactory, ChangeResource rsrc, SetPrivateOp.Input input)
+ BatchUpdate.Factory updateFactory, ChangeResource rsrc, @Nullable SetPrivateOp.Input input)
throws RestApiException, UpdateException {
if (!canDeletePrivate(rsrc).value()) {
throw new AuthException("not allowed to unmark private");
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
index da1679c..0bad054 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
@@ -15,8 +15,11 @@
package com.google.gerrit.server.restapi.change;
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
@@ -57,9 +60,10 @@
rsrc.getChangeResource().getProject(),
rsrc.getChangeResource().getUser(),
TimeUtil.nowTs())) {
+ bu.setNotify(getNotify(rsrc.getChange(), input));
BatchUpdateOp op;
if (rsrc.isByEmail()) {
- op = deleteReviewerByEmailOpFactory.create(rsrc.getReviewerByEmail(), input);
+ op = deleteReviewerByEmailOpFactory.create(rsrc.getReviewerByEmail());
} else {
op = deleteReviewerOpFactory.create(rsrc.getReviewerUser().state(), input);
}
@@ -68,4 +72,12 @@
}
return Response.none();
}
+
+ private static NotifyResolver.Result getNotify(Change change, DeleteReviewerInput input) {
+ NotifyHandling notifyHandling = input.notify;
+ if (notifyHandling == null) {
+ notifyHandling = change.isWorkInProgress() ? NotifyHandling.NONE : NotifyHandling.ALL;
+ }
+ return NotifyResolver.Result.create(notifyHandling);
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java
index 3231d16..0d41822 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewerByEmailOp.java
@@ -15,14 +15,12 @@
package com.google.gerrit.server.restapi.change;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.mail.Address;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.mail.send.DeleteReviewerSender;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -36,27 +34,20 @@
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public interface Factory {
- DeleteReviewerByEmailOp create(Address reviewer, DeleteReviewerInput input);
+ DeleteReviewerByEmailOp create(Address reviewer);
}
private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
- private final NotifyUtil notifyUtil;
private final Address reviewer;
- private final DeleteReviewerInput input;
private ChangeMessage changeMessage;
private Change change;
@Inject
DeleteReviewerByEmailOp(
- DeleteReviewerSender.Factory deleteReviewerSenderFactory,
- NotifyUtil notifyUtil,
- @Assisted Address reviewer,
- @Assisted DeleteReviewerInput input) {
+ DeleteReviewerSender.Factory deleteReviewerSenderFactory, @Assisted Address reviewer) {
this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
- this.notifyUtil = notifyUtil;
this.reviewer = reviewer;
- this.input = input;
}
@Override
@@ -79,24 +70,17 @@
@Override
public void postUpdate(Context ctx) {
- if (input.notify == null) {
- if (change.isWorkInProgress()) {
- input.notify = NotifyHandling.NONE;
- } else {
- input.notify = NotifyHandling.ALL;
- }
- }
- if (!NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
- return;
- }
try {
+ NotifyResolver.Result notify = ctx.getNotify(change.getId());
+ if (!notify.shouldNotify()) {
+ return;
+ }
DeleteReviewerSender cm =
deleteReviewerSenderFactory.create(ctx.getProject(), change.getId());
cm.setFrom(ctx.getAccountId());
cm.addReviewersByEmail(Collections.singleton(reviewer));
cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
- cm.setNotify(input.notify);
- cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+ cm.setNotify(notify);
cm.send();
} catch (Exception err) {
logger.atSevere().withCause(err).log("Cannot email update for change %s", change.getId());
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java
index 0cb4816..88f9679 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewerOp.java
@@ -18,6 +18,7 @@
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -27,13 +28,13 @@
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.extensions.events.ReviewerDeleted;
import com.google.gerrit.server.mail.send.DeleteReviewerSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -68,7 +69,6 @@
private final ReviewerDeleted reviewerDeleted;
private final Provider<IdentifiedUser> user;
private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
- private final NotifyUtil notifyUtil;
private final RemoveReviewerControl removeReviewerControl;
private final ProjectCache projectCache;
@@ -90,7 +90,6 @@
ReviewerDeleted reviewerDeleted,
Provider<IdentifiedUser> user,
DeleteReviewerSender.Factory deleteReviewerSenderFactory,
- NotifyUtil notifyUtil,
RemoveReviewerControl removeReviewerControl,
ProjectCache projectCache,
@Assisted AccountState reviewerAccount,
@@ -102,7 +101,6 @@
this.reviewerDeleted = reviewerDeleted;
this.user = user;
this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
- this.notifyUtil = notifyUtil;
this.removeReviewerControl = removeReviewerControl;
this.projectCache = projectCache;
this.reviewer = reviewerAccount;
@@ -169,15 +167,21 @@
@Override
public void postUpdate(Context ctx) {
- if (input.notify == null) {
- if (currChange.isWorkInProgress()) {
- input.notify = oldApprovals.isEmpty() ? NotifyHandling.NONE : NotifyHandling.OWNER;
- } else {
- input.notify = NotifyHandling.ALL;
- }
+ NotifyResolver.Result notify = ctx.getNotify(currChange.getId());
+ if (input.notify == null
+ && currChange.isWorkInProgress()
+ && !oldApprovals.isEmpty()
+ && notify.handling().compareTo(NotifyHandling.OWNER) < 0) {
+ // Override NotifyHandling from the context to notify owner if votes were removed on a WIP
+ // change.
+ notify = notify.withHandling(NotifyHandling.OWNER);
}
- if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
- emailReviewers(ctx.getProject(), currChange, changeMessage);
+ try {
+ if (notify.shouldNotify()) {
+ emailReviewers(ctx.getProject(), currChange, changeMessage, notify);
+ }
+ } catch (Exception err) {
+ logger.atSevere().withCause(err).log("Cannot email update for change %s", currChange.getId());
}
reviewerDeleted.fire(
currChange,
@@ -187,7 +191,7 @@
changeMessage.getMessage(),
newApprovals,
oldApprovals,
- input.notify,
+ notify.handling(),
ctx.getWhen());
}
@@ -206,22 +210,18 @@
}
private void emailReviewers(
- Project.NameKey projectName, Change change, ChangeMessage changeMessage) {
+ NameKey projectName, Change change, ChangeMessage changeMessage, NotifyResolver.Result notify)
+ throws EmailException {
Account.Id userId = user.get().getAccountId();
if (userId.equals(reviewer.getAccount().getId())) {
// The user knows they removed themselves, don't bother emailing them.
return;
}
- try {
- DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId());
- cm.setFrom(userId);
- cm.addReviewers(Collections.singleton(reviewer.getAccount().getId()));
- cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
- cm.setNotify(input.notify);
- cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
- cm.send();
- } catch (Exception err) {
- logger.atSevere().withCause(err).log("Cannot email update for change %s", change.getId());
- }
+ DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId());
+ cm.setFrom(userId);
+ cm.addReviewers(Collections.singleton(reviewer.getAccount().getId()));
+ cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
+ cm.setNotify(notify);
+ cm.send();
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index 60d1163..3a167bf 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.common.base.MoreObjects.firstNonNull;
import static java.util.Objects.requireNonNull;
import com.google.common.flogger.FluentLogger;
@@ -36,7 +37,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerResource;
import com.google.gerrit.server.change.VoteResource;
import com.google.gerrit.server.extensions.events.VoteDeleted;
@@ -61,6 +62,7 @@
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
+import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteInput, Response<?>> {
@@ -72,7 +74,7 @@
private final IdentifiedUser.GenericFactory userFactory;
private final VoteDeleted voteDeleted;
private final DeleteVoteSender.Factory deleteVoteSenderFactory;
- private final NotifyUtil notifyUtil;
+ private final NotifyResolver notifyResolver;
private final RemoveReviewerControl removeReviewerControl;
private final ProjectCache projectCache;
@@ -85,7 +87,7 @@
IdentifiedUser.GenericFactory userFactory,
VoteDeleted voteDeleted,
DeleteVoteSender.Factory deleteVoteSenderFactory,
- NotifyUtil notifyUtil,
+ NotifyResolver notifyResolver,
RemoveReviewerControl removeReviewerControl,
ProjectCache projectCache) {
super(retryHelper);
@@ -95,7 +97,7 @@
this.userFactory = userFactory;
this.voteDeleted = voteDeleted;
this.deleteVoteSenderFactory = deleteVoteSenderFactory;
- this.notifyUtil = notifyUtil;
+ this.notifyResolver = notifyResolver;
this.removeReviewerControl = removeReviewerControl;
this.projectCache = projectCache;
}
@@ -103,7 +105,7 @@
@Override
protected Response<?> applyImpl(
BatchUpdate.Factory updateFactory, VoteResource rsrc, DeleteVoteInput input)
- throws RestApiException, UpdateException, IOException {
+ throws RestApiException, UpdateException, IOException, OrmException, ConfigInvalidException {
if (input == null) {
input = new DeleteVoteInput();
}
@@ -123,6 +125,9 @@
try (BatchUpdate bu =
updateFactory.create(
change.getProject(), r.getChangeResource().getUser(), TimeUtil.nowTs())) {
+ bu.setNotify(
+ notifyResolver.resolve(
+ firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
bu.addOp(
change.getId(),
new Op(
@@ -217,17 +222,17 @@
}
IdentifiedUser user = ctx.getIdentifiedUser();
- if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
- try {
+ try {
+ NotifyResolver.Result notify = ctx.getNotify(change.getId());
+ if (notify.shouldNotify()) {
ReplyToChangeSender cm = deleteVoteSenderFactory.create(ctx.getProject(), change.getId());
cm.setFrom(user.getAccountId());
cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen());
- cm.setNotify(input.notify);
- cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+ cm.setNotify(notify);
cm.send();
- } catch (Exception e) {
- logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
}
+ } catch (Exception e) {
+ logger.atSevere().withCause(e).log("Cannot email update for change %s", change.getId());
}
voteDeleted.fire(
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index d2913ef..62b2bcf 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -29,7 +29,6 @@
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
-import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
@@ -42,7 +41,6 @@
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AddReviewerResult;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
@@ -90,7 +88,7 @@
import com.google.gerrit.server.change.AddReviewersEmail;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.EmailReviewComments;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerAdder;
import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
import com.google.gerrit.server.change.RevisionResource;
@@ -169,7 +167,7 @@
private final CommentAdded commentAdded;
private final ReviewerAdder reviewerAdder;
private final AddReviewersEmail addReviewersEmail;
- private final NotifyUtil notifyUtil;
+ private final NotifyResolver notifyResolver;
private final Config gerritConfig;
private final WorkInProgressOp.Factory workInProgressOpFactory;
private final ProjectCache projectCache;
@@ -192,7 +190,7 @@
CommentAdded commentAdded,
ReviewerAdder reviewerAdder,
AddReviewersEmail addReviewersEmail,
- NotifyUtil notifyUtil,
+ NotifyResolver notifyResolver,
@GerritServerConfig Config gerritConfig,
WorkInProgressOp.Factory workInProgressOpFactory,
ProjectCache projectCache,
@@ -211,7 +209,7 @@
this.commentAdded = commentAdded;
this.reviewerAdder = reviewerAdder;
this.addReviewersEmail = addReviewersEmail;
- this.notifyUtil = notifyUtil;
+ this.notifyResolver = notifyResolver;
this.gerritConfig = gerritConfig;
this.workInProgressOpFactory = workInProgressOpFactory;
this.projectCache = projectCache;
@@ -253,14 +251,10 @@
checkRobotComments(revision, input.robotComments);
}
- NotifyHandling reviewerNotify = input.notify;
if (input.notify == null) {
input.notify = defaultNotify(revision.getChange(), input);
}
- ListMultimap<RecipientType, Account.Id> accountsToNotify =
- notifyUtil.resolveAccounts(input.notifyDetails);
-
Map<String, AddReviewerResult> reviewerJsonResults = null;
List<ReviewerAddition> reviewerResults = Lists.newArrayList();
boolean hasError = false;
@@ -268,12 +262,6 @@
if (input.reviewers != null) {
reviewerJsonResults = Maps.newHashMap();
for (AddReviewerInput reviewerInput : input.reviewers) {
- // Prevent individual AddReviewersOps from sending one email each. Instead, we call
- // batchEmailReviewers at the very end to send out a single email.
- // TODO(dborowitz): I think this still sends out separate emails if any of input.reviewers
- // specifies explicit accountsToNotify. Unclear whether that's a good thing.
- reviewerInput.notify = NotifyHandling.NONE;
-
ReviewerAddition result =
reviewerAdder.prepare(revision.getNotes(), revision.getUser(), reviewerInput, true);
reviewerJsonResults.put(reviewerInput.reviewer, result.result);
@@ -316,6 +304,7 @@
// updated set of reviewers. Also keep track of whether the user added
// themselves as a reviewer or to the CC list.
for (ReviewerAddition reviewerResult : reviewerResults) {
+ reviewerResult.op.suppressEmail(); // Send a single batch email below.
bu.addOp(revision.getChange().getId(), reviewerResult.op);
if (!ccOrReviewer && reviewerResult.result.reviewers != null) {
for (ReviewerInfo reviewerInfo : reviewerResult.result.reviewers) {
@@ -340,6 +329,7 @@
// isn't being explicitly added, and isn't voting on any label.
// Automatically CC them on this change so they receive replies.
ReviewerAddition selfAddition = reviewerAdder.ccCurrentUser(revision.getUser(), revision);
+ selfAddition.op.suppressEmail();
bu.addOp(revision.getChange().getId(), selfAddition.op);
}
@@ -357,19 +347,21 @@
output.ready = true;
}
- // Suppress notifications in WorkInProgressOp, we'll take care of
- // them in this endpoint.
- WorkInProgressOp.Input wipIn = new WorkInProgressOp.Input();
- wipIn.notify = NotifyHandling.NONE;
- bu.addOp(
- revision.getChange().getId(),
- workInProgressOpFactory.create(input.workInProgress, wipIn));
+ WorkInProgressOp wipOp =
+ workInProgressOpFactory.create(input.workInProgress, new WorkInProgressOp.Input());
+ wipOp.suppressEmail();
+ bu.addOp(revision.getChange().getId(), wipOp);
}
// Add the review op.
bu.addOp(
revision.getChange().getId(),
- new Op(projectState, revision.getPatchSet().getId(), input, accountsToNotify));
+ new Op(projectState, revision.getPatchSet().getId(), input));
+
+ // Notify based on ReviewInput, ignoring the notify settings from any AddReviewerInputs.
+ NotifyResolver.Result notify =
+ notifyResolver.resolve(getNotifyHandling(input, output, revision), input.notifyDetails);
+ bu.setNotify(notify);
bu.execute();
@@ -379,21 +371,24 @@
reviewerResult.gatherResults(cd);
}
- boolean readyForReview =
- (output.ready != null && output.ready) || !revision.getChange().isWorkInProgress();
// Sending from AddReviewersOp was suppressed so we can send a single batch email here.
- batchEmailReviewers(
- revision.getUser(),
- revision.getChange(),
- reviewerResults,
- reviewerNotify,
- accountsToNotify,
- readyForReview);
+ batchEmailReviewers(revision.getUser(), revision.getChange(), reviewerResults, notify);
}
return Response.ok(output);
}
+ private NotifyHandling getNotifyHandling(
+ ReviewInput input, ReviewResult output, RevisionResource revision) {
+ if (input.notify != null) {
+ return input.notify;
+ }
+ if ((output.ready != null && output.ready) || !revision.getChange().isWorkInProgress()) {
+ return NotifyHandling.ALL;
+ }
+ return NotifyHandling.NONE;
+ }
+
private NotifyHandling defaultNotify(Change c, ReviewInput in) {
boolean workInProgress = c.isWorkInProgress();
if (in.workInProgress) {
@@ -409,11 +404,12 @@
}
if (workInProgress && !c.hasReviewStarted()) {
- // If review hasn't started we want to minimize recipients, no matter who
- // the author is.
- return NotifyHandling.OWNER;
+ // If review hasn't started we want to eliminate notifications, no matter who the author is.
+ return NotifyHandling.NONE;
}
+ // Otherwise, it's either a non-WIP change, or a WIP change where review has started. Notify
+ // everyone.
return NotifyHandling.ALL;
}
@@ -421,9 +417,7 @@
CurrentUser user,
Change change,
List<ReviewerAddition> reviewerAdditions,
- @Nullable NotifyHandling notify,
- ListMultimap<RecipientType, Account.Id> accountsToNotify,
- boolean readyForReview) {
+ NotifyResolver.Result notify) {
List<Account.Id> to = new ArrayList<>();
List<Account.Id> cc = new ArrayList<>();
List<Address> toByEmail = new ArrayList<>();
@@ -438,15 +432,7 @@
}
}
addReviewersEmail.emailReviewers(
- user.asIdentifiedUser(),
- change,
- to,
- cc,
- toByEmail,
- ccByEmail,
- notify,
- accountsToNotify,
- readyForReview);
+ user.asIdentifiedUser(), change, to, cc, toByEmail, ccByEmail, notify);
}
private RevisionResource onBehalfOf(RevisionResource rev, LabelTypes labelTypes, ReviewInput in)
@@ -491,7 +477,7 @@
String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf));
}
- IdentifiedUser reviewer = accountResolver.parseOnBehalfOf(caller, in.onBehalfOf);
+ IdentifiedUser reviewer = accountResolver.resolve(in.onBehalfOf).asUniqueUserOnBehalfOf(caller);
try {
permissionBackend.user(reviewer).change(rev.getNotes()).check(ChangePermission.READ);
} catch (AuthException e) {
@@ -849,7 +835,6 @@
private final ProjectState projectState;
private final PatchSet.Id psId;
private final ReviewInput in;
- private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
private IdentifiedUser user;
private ChangeNotes notes;
@@ -860,15 +845,10 @@
private Map<String, Short> approvals = new HashMap<>();
private Map<String, Short> oldApprovals = new HashMap<>();
- private Op(
- ProjectState projectState,
- PatchSet.Id psId,
- ReviewInput in,
- ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+ private Op(ProjectState projectState, PatchSet.Id psId, ReviewInput in) {
this.projectState = projectState;
this.psId = psId;
this.in = in;
- this.accountsToNotify = requireNonNull(accountsToNotify);
}
@Override
@@ -891,18 +871,10 @@
if (message == null) {
return;
}
- if (in.notify.compareTo(NotifyHandling.NONE) > 0 || !accountsToNotify.isEmpty()) {
+ NotifyResolver.Result notify = ctx.getNotify(notes.getChangeId());
+ if (notify.shouldNotify()) {
email
- .create(
- in.notify,
- accountsToNotify,
- notes,
- ps,
- user,
- message,
- comments,
- in.message,
- labelDelta)
+ .create(notify, notes, ps, user, message, comments, in.message, labelDelta)
.sendAsync();
}
commentAdded.fire(
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index fdfefab..4aeb07f 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -16,10 +16,12 @@
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AddReviewerResult;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ReviewerAdder;
import com.google.gerrit.server.change.ReviewerAdder.ReviewerAddition;
import com.google.gerrit.server.change.ReviewerResource;
@@ -42,13 +44,18 @@
ChangeResource, ReviewerResource, AddReviewerInput, AddReviewerResult> {
private final ChangeData.Factory changeDataFactory;
+ private final NotifyResolver notifyResolver;
private final ReviewerAdder reviewerAdder;
@Inject
PostReviewers(
- ChangeData.Factory changeDataFactory, RetryHelper retryHelper, ReviewerAdder reviewerAdder) {
+ ChangeData.Factory changeDataFactory,
+ RetryHelper retryHelper,
+ NotifyResolver notifyResolver,
+ ReviewerAdder reviewerAdder) {
super(retryHelper);
this.changeDataFactory = changeDataFactory;
+ this.notifyResolver = notifyResolver;
this.reviewerAdder = reviewerAdder;
}
@@ -67,6 +74,7 @@
}
try (BatchUpdate bu =
updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ bu.setNotify(resolveNotify(rsrc, input));
Change.Id id = rsrc.getChange().getId();
bu.addOp(id, addition.op);
bu.execute();
@@ -76,4 +84,14 @@
addition.gatherResults(changeDataFactory.create(rsrc.getProject(), rsrc.getId()));
return addition.result;
}
+
+ private NotifyResolver.Result resolveNotify(ChangeResource rsrc, AddReviewerInput input)
+ throws BadRequestException, OrmException, ConfigInvalidException, IOException {
+ NotifyHandling notifyHandling = input.notify;
+ if (notifyHandling == null) {
+ notifyHandling =
+ rsrc.getChange().isWorkInProgress() ? NotifyHandling.NONE : NotifyHandling.ALL;
+ }
+ return notifyResolver.resolve(notifyHandling, input.notifyDetails);
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
index 3d401c4..b0cad84 100644
--- a/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
+++ b/java/com/google/gerrit/server/restapi/change/PublishChangeEdit.java
@@ -14,12 +14,15 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.common.base.MoreObjects.firstNonNull;
+
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.edit.ChangeEdit;
import com.google.gerrit.server.edit.ChangeEditUtil;
import com.google.gerrit.server.project.ContributorAgreementsChecker;
@@ -39,18 +42,18 @@
public class PublishChangeEdit
extends RetryingRestModifyView<ChangeResource, PublishChangeEditInput, Response<?>> {
private final ChangeEditUtil editUtil;
- private final NotifyUtil notifyUtil;
+ private final NotifyResolver notifyResolver;
private final ContributorAgreementsChecker contributorAgreementsChecker;
@Inject
PublishChangeEdit(
RetryHelper retryHelper,
ChangeEditUtil editUtil,
- NotifyUtil notifyUtil,
+ NotifyResolver notifyResolver,
ContributorAgreementsChecker contributorAgreementsChecker) {
super(retryHelper);
this.editUtil = editUtil;
- this.notifyUtil = notifyUtil;
+ this.notifyResolver = notifyResolver;
this.contributorAgreementsChecker = contributorAgreementsChecker;
}
@@ -73,8 +76,7 @@
rsrc.getNotes(),
rsrc.getUser(),
edit.get(),
- in.notify,
- notifyUtil.resolveAccounts(in.notifyDetails));
+ notifyResolver.resolve(firstNonNull(in.notify, NotifyHandling.ALL), in.notifyDetails));
return Response.none();
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/PutAssignee.java b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
index 982b3e6..a7dcc12 100644
--- a/java/com/google/gerrit/server/restapi/change/PutAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
@@ -23,7 +23,6 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountLoader;
@@ -84,10 +83,7 @@
throw new BadRequestException("missing assignee field");
}
- IdentifiedUser assignee = accountResolver.parse(input.assignee);
- if (!assignee.getAccount().isActive()) {
- throw new UnprocessableEntityException(input.assignee + " is not active");
- }
+ IdentifiedUser assignee = accountResolver.resolve(input.assignee).asUniqueUser();
try {
permissionBackend
.absentUser(assignee.getAccountId())
@@ -103,6 +99,7 @@
bu.addOp(rsrc.getId(), op);
ReviewerAddition reviewersAddition = addAssigneeAsCC(rsrc, input.assignee);
+ reviewersAddition.op.suppressEmail();
bu.addOp(rsrc.getId(), reviewersAddition.op);
bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index 80d0aba..39dfc7f 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -29,7 +29,7 @@
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -71,7 +71,7 @@
private final PatchSetInserter.Factory psInserterFactory;
private final PermissionBackend permissionBackend;
private final PatchSetUtil psUtil;
- private final NotifyUtil notifyUtil;
+ private final NotifyResolver notifyResolver;
private final ProjectCache projectCache;
@Inject
@@ -83,7 +83,7 @@
PermissionBackend permissionBackend,
@GerritPersonIdent PersonIdent gerritIdent,
PatchSetUtil psUtil,
- NotifyUtil notifyUtil,
+ NotifyResolver notifyResolver,
ProjectCache projectCache) {
super(retryHelper);
this.repositoryManager = repositoryManager;
@@ -92,7 +92,7 @@
this.tz = gerritIdent.getTimeZone();
this.permissionBackend = permissionBackend;
this.psUtil = psUtil;
- this.notifyUtil = notifyUtil;
+ this.notifyResolver = notifyResolver;
this.projectCache = projectCache;
}
@@ -117,11 +117,6 @@
resource.getChange().getKey().get(),
sanitizedCommitMessage);
- NotifyHandling notify = input.notify;
- if (notify == null) {
- notify = resource.getChange().isWorkInProgress() ? NotifyHandling.OWNER : NotifyHandling.ALL;
- }
-
try (Repository repository = repositoryManager.openRepository(resource.getProject());
RevWalk revWalk = new RevWalk(repository);
ObjectInserter objectInserter = repository.newObjectInserter()) {
@@ -145,8 +140,7 @@
inserter.setMessage(
String.format("Patch Set %s: Commit message was updated.", psId.getId()));
inserter.setDescription("Edit commit message");
- inserter.setNotify(notify);
- inserter.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+ bu.setNotify(resolveNotify(input, resource));
bu.addOp(resource.getChange().getId(), inserter);
bu.execute();
}
@@ -154,6 +148,16 @@
return Response.ok("ok");
}
+ private NotifyResolver.Result resolveNotify(CommitMessageInput input, ChangeResource resource)
+ throws BadRequestException, OrmException, ConfigInvalidException, IOException {
+ NotifyHandling notifyHandling = input.notify;
+ if (notifyHandling == null) {
+ notifyHandling =
+ resource.getChange().isWorkInProgress() ? NotifyHandling.OWNER : NotifyHandling.ALL;
+ }
+ return notifyResolver.resolve(notifyHandling, input.notifyDetails);
+ }
+
private ObjectId createCommit(
ObjectInserter objectInserter,
RevCommit basePatchSetCommit,
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index de9e990..fe319bf 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -33,6 +33,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.change.RebaseUtil;
import com.google.gerrit.server.change.RebaseUtil.Base;
@@ -120,6 +121,8 @@
throw new ResourceConflictException(
"cannot rebase merge commits or commit with no ancestor");
}
+ // TODO(dborowitz): Why no notification? This seems wrong; dig up blame.
+ bu.setNotify(NotifyResolver.Result.none());
bu.setRepository(repo, rw, oi);
bu.addOp(
change.getId(),
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index b070b1c..d2939d1 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -14,14 +14,13 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
import com.google.common.base.Strings;
-import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.RevertInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -44,7 +43,7 @@
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeMessages;
import com.google.gerrit.server.change.ChangeResource;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.extensions.events.ChangeReverted;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.mail.send.RevertedSender;
@@ -104,7 +103,7 @@
private final ChangeReverted changeReverted;
private final ContributorAgreementsChecker contributorAgreements;
private final ProjectCache projectCache;
- private final NotifyUtil notifyUtil;
+ private final NotifyResolver notifyResolver;
@Inject
Revert(
@@ -122,7 +121,7 @@
ChangeReverted changeReverted,
ContributorAgreementsChecker contributorAgreements,
ProjectCache projectCache,
- NotifyUtil notifyUtil) {
+ NotifyResolver notifyResolver) {
super(retryHelper);
this.permissionBackend = permissionBackend;
this.repoManager = repoManager;
@@ -137,7 +136,7 @@
this.changeReverted = changeReverted;
this.contributorAgreements = contributorAgreements;
this.projectCache = projectCache;
- this.notifyUtil = notifyUtil;
+ this.notifyResolver = notifyResolver;
}
@Override
@@ -216,16 +215,15 @@
ObjectId id = oi.insert(revertCommitBuilder);
RevCommit revertCommit = revWalk.parseCommit(id);
- ListMultimap<RecipientType, Account.Id> accountsToNotify =
- notifyUtil.resolveAccounts(input.notifyDetails);
+ NotifyResolver.Result notify =
+ notifyResolver.resolve(
+ firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails);
ChangeInserter ins =
changeInserterFactory
.create(changeId, revertCommit, notes.getChange().getDest().get())
.setTopic(changeToRevert.getTopic());
ins.setMessage("Uploaded patch set 1.");
- ins.setNotify(input.notify);
- ins.setAccountsToNotify(accountsToNotify);
ReviewerSet reviewerSet = approvalsUtil.getReviewers(notes);
@@ -240,8 +238,9 @@
try (BatchUpdate bu = updateFactory.create(project, user, now)) {
bu.setRepository(git, revWalk, oi);
+ bu.setNotify(notify);
bu.insertChange(ins);
- bu.addOp(changeId, new NotifyOp(changeToRevert, ins, input.notify, accountsToNotify));
+ bu.addOp(changeId, new NotifyOp(changeToRevert, ins));
bu.addOp(changeToRevert.getId(), new PostRevertedMessageOp(computedChangeId));
bu.execute();
}
@@ -276,18 +275,10 @@
private class NotifyOp implements BatchUpdateOp {
private final Change change;
private final ChangeInserter ins;
- private final NotifyHandling notifyHandling;
- private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
- NotifyOp(
- Change change,
- ChangeInserter ins,
- NotifyHandling notifyHandling,
- ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+ NotifyOp(Change change, ChangeInserter ins) {
this.change = change;
this.ins = ins;
- this.notifyHandling = notifyHandling;
- this.accountsToNotify = accountsToNotify;
}
@Override
@@ -296,8 +287,7 @@
try {
RevertedSender cm = revertedSenderFactory.create(ctx.getProject(), change.getId());
cm.setFrom(ctx.getAccountId());
- cm.setNotify(notifyHandling);
- cm.setAccountsToNotify(accountsToNotify);
+ cm.setNotify(ctx.getNotify(change.getId()));
cm.send();
} catch (Exception err) {
logger.atSevere().withCause(err).log(
diff --git a/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java b/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java
index 04c94be..3bb297d 100644
--- a/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java
+++ b/java/com/google/gerrit/server/restapi/change/SetPrivateOp.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.restapi.change;
import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -43,14 +44,14 @@
}
public interface Factory {
- SetPrivateOp create(ChangeMessagesUtil cmUtil, boolean isPrivate, Input input);
+ SetPrivateOp create(ChangeMessagesUtil cmUtil, boolean isPrivate, @Nullable Input input);
}
- private final ChangeMessagesUtil cmUtil;
- private final PatchSetUtil psUtil;
- private final boolean isPrivate;
- private final Input input;
private final PrivateStateChanged privateStateChanged;
+ private final PatchSetUtil psUtil;
+ private final ChangeMessagesUtil cmUtil;
+ private final boolean isPrivate;
+ @Nullable private final Input input;
private Change change;
private PatchSet ps;
@@ -61,12 +62,12 @@
PatchSetUtil psUtil,
@Assisted ChangeMessagesUtil cmUtil,
@Assisted boolean isPrivate,
- @Assisted Input input) {
- this.cmUtil = cmUtil;
+ @Assisted @Nullable Input input) {
+ this.privateStateChanged = privateStateChanged;
this.psUtil = psUtil;
+ this.cmUtil = cmUtil;
this.isPrivate = isPrivate;
this.input = input;
- this.privateStateChanged = privateStateChanged;
}
@Override
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index 6d25d938..a3299b5 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -14,9 +14,11 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -26,6 +28,7 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.WorkInProgressOp;
import com.google.gerrit.server.change.WorkInProgressOp.Input;
import com.google.gerrit.server.permissions.GlobalPermission;
@@ -77,6 +80,7 @@
try (BatchUpdate bu =
updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.ALL)));
bu.addOp(rsrc.getChange().getId(), opFactory.create(false, input));
bu.execute();
return Response.ok("");
diff --git a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
index a23f591..0bf37a7 100644
--- a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
+++ b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
@@ -14,9 +14,11 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
import static com.google.gerrit.extensions.conditions.BooleanCondition.or;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -26,6 +28,7 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.WorkInProgressOp;
import com.google.gerrit.server.change.WorkInProgressOp.Input;
import com.google.gerrit.server.permissions.GlobalPermission;
@@ -77,6 +80,7 @@
try (BatchUpdate bu =
updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+ bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.NONE)));
bu.addOp(rsrc.getChange().getId(), opFactory.create(true, input));
bu.execute();
return Response.ok("");
diff --git a/java/com/google/gerrit/server/restapi/change/Submit.java b/java/com/google/gerrit/server/restapi/change/Submit.java
index a0e9d70..7a60b3b 100644
--- a/java/com/google/gerrit/server/restapi/change/Submit.java
+++ b/java/com/google/gerrit/server/restapi/change/Submit.java
@@ -463,7 +463,8 @@
perm.check(ChangePermission.SUBMIT_AS);
CurrentUser caller = rsrc.getUser();
- IdentifiedUser submitter = accountResolver.parseOnBehalfOf(caller, in.onBehalfOf);
+ IdentifiedUser submitter =
+ accountResolver.resolve(in.onBehalfOf).asUniqueUserOnBehalfOf(caller);
try {
permissionBackend.user(submitter).change(rsrc.getNotes()).check(ChangePermission.READ);
} catch (AuthException e) {
diff --git a/java/com/google/gerrit/server/restapi/group/AddMembers.java b/java/com/google/gerrit/server/restapi/group/AddMembers.java
index bdf1c74..2d2d02b 100644
--- a/java/com/google/gerrit/server/restapi/group/AddMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/AddMembers.java
@@ -37,6 +37,7 @@
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.GroupControl;
@@ -144,19 +145,18 @@
}
Account findAccount(String nameOrEmailOrId)
- throws AuthException, UnprocessableEntityException, OrmException, IOException,
- ConfigInvalidException {
+ throws UnprocessableEntityException, OrmException, IOException, ConfigInvalidException {
+ AccountResolver.Result result = accountResolver.resolve(nameOrEmailOrId);
try {
- return accountResolver.parse(nameOrEmailOrId).getAccount();
- } catch (UnprocessableEntityException e) {
- // might be because the account does not exist or because the account is
- // not visible
+ return result.asUnique().getAccount();
+ } catch (UnresolvableAccountException e) {
switch (authType) {
case HTTP_LDAP:
case CLIENT_SSL_CERT_LDAP:
case LDAP:
- if (accountResolver.find(nameOrEmailOrId) == null) {
- // account does not exist, try to create it
+ if (!e.isSelf() && result.asList().isEmpty()) {
+ // Account does not exist, try to create it. This may leak account existence, since we
+ // can't distinguish between a nonexistent account and one that the caller can't see.
Optional<Account> a = createAccountByLdap(nameOrEmailOrId);
if (a.isPresent()) {
return a.get();
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
index d197cb8..fca88e7 100644
--- a/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
+++ b/java/com/google/gerrit/server/restapi/group/DeleteMembers.java
@@ -69,8 +69,7 @@
Set<Account.Id> membersToRemove = new HashSet<>();
for (String nameOrEmail : input.members) {
- Account a = accountResolver.parse(nameOrEmail).getAccount();
- membersToRemove.add(a.getId());
+ membersToRemove.add(accountResolver.resolve(nameOrEmail).asUnique().getAccount().getId());
}
AccountGroup.UUID groupUuid = internalGroup.getGroupUUID();
try {
diff --git a/java/com/google/gerrit/server/restapi/group/Module.java b/java/com/google/gerrit/server/restapi/group/Module.java
index 741c3da..45ac411 100644
--- a/java/com/google/gerrit/server/restapi/group/Module.java
+++ b/java/com/google/gerrit/server/restapi/group/Module.java
@@ -82,7 +82,7 @@
@Provides
@ServerInitiated
GroupsUpdate provideServerInitiatedGroupsUpdate(GroupsUpdate.Factory groupsUpdateFactory) {
- return groupsUpdateFactory.create(null);
+ return groupsUpdateFactory.createWithServerIdent();
}
@Provides
diff --git a/java/com/google/gerrit/server/restapi/project/CheckAccess.java b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
index 1664635..51e0304 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckAccess.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckAccess.java
@@ -23,7 +23,6 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.server.account.AccountResolver;
@@ -75,20 +74,16 @@
throw new BadRequestException("input requires 'account'");
}
- Account match = accountResolver.find(input.account);
- if (match == null) {
- throw new UnprocessableEntityException(
- String.format("cannot find account %s", input.account));
- }
+ Account.Id match = accountResolver.resolve(input.account).asUnique().getAccount().getId();
AccessCheckInfo info = new AccessCheckInfo();
try {
permissionBackend
- .absentUser(match.getId())
+ .absentUser(match)
.project(rsrc.getNameKey())
.check(ProjectPermission.ACCESS);
} catch (AuthException e) {
- info.message = String.format("user %s cannot see project %s", match.getId(), rsrc.getName());
+ info.message = String.format("user %s cannot see project %s", match, rsrc.getName());
info.status = HttpServletResponse.SC_FORBIDDEN;
return info;
}
@@ -112,7 +107,7 @@
if (!Strings.isNullOrEmpty(input.ref)) {
try {
permissionBackend
- .absentUser(match.getId())
+ .absentUser(match)
.ref(new Branch.NameKey(rsrc.getNameKey(), input.ref))
.check(refPerm);
} catch (AuthException e) {
@@ -120,7 +115,7 @@
info.message =
String.format(
"user %s lacks permission %s for %s in project %s",
- match.getId(), input.permission, input.ref, rsrc.getName());
+ match, input.permission, input.ref, rsrc.getName());
return info;
}
} else {
diff --git a/java/com/google/gerrit/server/restapi/project/Index.java b/java/com/google/gerrit/server/restapi/project/Index.java
index 1b2a523..a346aed 100644
--- a/java/com/google/gerrit/server/restapi/project/Index.java
+++ b/java/com/google/gerrit/server/restapi/project/Index.java
@@ -22,9 +22,8 @@
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.api.projects.IndexProjectInput;
import com.google.gerrit.extensions.common.ProjectInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.index.project.ProjectIndexer;
import com.google.gerrit.reviewdb.client.Project;
@@ -59,15 +58,12 @@
@Override
public Response.Accepted apply(ProjectResource rsrc, IndexProjectInput input)
- throws IOException, AuthException, OrmException, PermissionBackendException,
- ResourceConflictException {
+ throws IOException, OrmException, PermissionBackendException, RestApiException {
String response = "Project " + rsrc.getName() + " submitted for reindexing";
reindex(rsrc.getNameKey(), input.async);
if (Boolean.TRUE.equals(input.indexChildren)) {
- ListChildProjects listChildProjects = listChildProjectsProvider.get();
- listChildProjects.setRecursive(true);
- for (ProjectInfo child : listChildProjects.apply(rsrc)) {
+ for (ProjectInfo child : listChildProjectsProvider.get().withRecursive(true).apply(rsrc)) {
reindex(new Project.NameKey(child.name), input.async);
}
diff --git a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
index 3067c89..cfeec5a 100644
--- a/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListChildProjects.java
@@ -17,22 +17,20 @@
import static java.util.stream.Collectors.toList;
import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsName;
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.project.ChildProjects;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectJson;
import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
-import java.util.HashMap;
+import com.google.inject.Provider;
import java.util.List;
-import java.util.Map;
import org.kohsuke.args4j.Option;
public class ListChildProjects implements RestReadView<ProjectResource> {
@@ -40,33 +38,42 @@
@Option(name = "--recursive", usage = "to list child projects recursively")
private boolean recursive;
- private final ProjectCache projectCache;
+ @Option(name = "--limit", usage = "maximum number of parents projects to list")
+ private int limit;
+
private final PermissionBackend permissionBackend;
- private final AllProjectsName allProjects;
- private final ProjectJson json;
private final ChildProjects childProjects;
+ private final Provider<QueryProjects> queryProvider;
@Inject
ListChildProjects(
- ProjectCache projectCache,
PermissionBackend permissionBackend,
- AllProjectsName allProjectsName,
- ProjectJson json,
- ChildProjects childProjects) {
- this.projectCache = projectCache;
+ ChildProjects childProjects,
+ Provider<QueryProjects> queryProvider) {
this.permissionBackend = permissionBackend;
- this.allProjects = allProjectsName;
- this.json = json;
this.childProjects = childProjects;
+ this.queryProvider = queryProvider;
}
- public void setRecursive(boolean recursive) {
+ public ListChildProjects withRecursive(boolean recursive) {
this.recursive = recursive;
+ return this;
+ }
+
+ public ListChildProjects withLimit(int limit) {
+ this.limit = limit;
+ return this;
}
@Override
public List<ProjectInfo> apply(ProjectResource rsrc)
- throws PermissionBackendException, ResourceConflictException {
+ throws PermissionBackendException, OrmException, RestApiException {
+ if (limit < 0) {
+ throw new BadRequestException("limit must be a positive number");
+ }
+ if (recursive && limit != 0) {
+ throw new ResourceConflictException("recursive and limit options are mutually exclusive");
+ }
rsrc.getProjectState().checkStatePermitsRead();
if (recursive) {
return childProjects.list(rsrc.getNameKey());
@@ -76,22 +83,19 @@
}
private List<ProjectInfo> directChildProjects(Project.NameKey parent)
- throws PermissionBackendException {
- Map<Project.NameKey, Project> children = new HashMap<>();
- for (Project.NameKey name : projectCache.all()) {
- ProjectState c = projectCache.get(name);
- if (c != null
- && parent.equals(c.getProject().getParent(allProjects))
- && c.statePermitsRead()) {
- children.put(c.getNameKey(), c.getProject());
- }
- }
- return permissionBackend
- .currentUser()
- .filter(ProjectPermission.ACCESS, children.keySet())
+ throws OrmException, RestApiException {
+ PermissionBackend.WithUser currentUser = permissionBackend.currentUser();
+ return queryProvider
+ .get()
+ .withQuery("parent:" + parent.get())
+ .withLimit(limit)
+ .apply()
.stream()
- .sorted()
- .map((p) -> json.format(children.get(p)))
+ .filter(
+ p ->
+ currentUser
+ .project(new Project.NameKey(p.name))
+ .testOrFalse(ProjectPermission.ACCESS))
.collect(toList());
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index 35d1ca1..4bf1230 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -14,11 +14,15 @@
package com.google.gerrit.server.restapi.project;
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static com.google.common.collect.Ordering.natural;
import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
import static java.nio.charset.StandardCharsets.UTF_8;
-import com.google.common.base.Strings;
+import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
@@ -29,6 +33,7 @@
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.Url;
@@ -51,7 +56,9 @@
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.util.TreeFormatter;
import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -67,6 +74,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
@@ -244,6 +252,7 @@
private String matchSubstring;
private String matchRegex;
private AccountGroup.UUID groupUuid;
+ private final Provider<QueryProjects> queryProjectsProvider;
@Inject
protected ListProjects(
@@ -254,7 +263,8 @@
GitRepositoryManager repoManager,
PermissionBackend permissionBackend,
ProjectNode.Factory projectNodeFactory,
- WebLinks webLinks) {
+ WebLinks webLinks,
+ Provider<QueryProjects> queryProjectsProvider) {
this.currentUser = currentUser;
this.projectCache = projectCache;
this.groupResolver = groupResolver;
@@ -263,6 +273,7 @@
this.permissionBackend = permissionBackend;
this.projectNodeFactory = projectNodeFactory;
this.webLinks = webLinks;
+ this.queryProjectsProvider = queryProjectsProvider;
}
public List<String> getShowBranch() {
@@ -291,7 +302,7 @@
throws BadRequestException, PermissionBackendException {
if (format == OutputFormat.TEXT) {
ByteArrayOutputStream buf = new ByteArrayOutputStream();
- display(buf);
+ displayToStream(buf);
return BinaryResult.create(buf.toByteArray())
.setContentType("text/plain")
.setCharacterEncoding(UTF_8);
@@ -301,11 +312,104 @@
public SortedMap<String, ProjectInfo> apply()
throws BadRequestException, PermissionBackendException {
+ Optional<String> projectQuery = expressAsProjectsQuery();
+ if (projectQuery.isPresent()) {
+ return applyAsQuery(projectQuery.get());
+ }
+
format = OutputFormat.JSON;
return display(null);
}
- public SortedMap<String, ProjectInfo> display(@Nullable OutputStream displayOutputStream)
+ private Optional<String> expressAsProjectsQuery() {
+ return !all
+ && state != HIDDEN
+ && isNullOrEmpty(matchPrefix)
+ && isNullOrEmpty(matchRegex)
+ && isNullOrEmpty(matchSubstring) // TODO: see Issue 10446
+ && type == FilterType.ALL
+ && showBranch.isEmpty()
+ && !showTree
+ ? Optional.of(stateToQuery())
+ : Optional.empty();
+ }
+
+ private String stateToQuery() {
+ List<String> queries = new ArrayList<>();
+ if (state == null) {
+ queries.add("(state:active OR state:read-only)");
+ } else {
+ queries.add(String.format("(state:%s)", state.name()));
+ }
+
+ return Joiner.on(" AND ").join(queries).toString();
+ }
+
+ private SortedMap<String, ProjectInfo> applyAsQuery(String query) throws BadRequestException {
+ try {
+ return queryProjectsProvider
+ .get()
+ .withQuery(query)
+ .withStart(start)
+ .withLimit(limit)
+ .apply()
+ .stream()
+ .collect(
+ ImmutableSortedMap.toImmutableSortedMap(
+ natural(), p -> p.name, p -> showDescription ? p : nullifyDescription(p)));
+ } catch (OrmException | MethodNotAllowedException e) {
+ logger.atWarning().withCause(e).log(
+ "Internal error while processing the query '%s' request", query);
+ throw new BadRequestException("Internal error while processing the query request");
+ }
+ }
+
+ private ProjectInfo nullifyDescription(ProjectInfo p) {
+ p.description = null;
+ return p;
+ }
+
+ private void printQueryResults(String query, PrintWriter out) throws BadRequestException {
+ try {
+ if (format.isJson()) {
+ format.newGson().toJson(applyAsQuery(query), out);
+ } else {
+ newProjectsNamesStream(query).forEach(out::println);
+ }
+ out.flush();
+ } catch (OrmException | MethodNotAllowedException e) {
+ logger.atWarning().withCause(e).log(
+ "Internal error while processing the query '%s' request", query);
+ throw new BadRequestException("Internal error while processing the query request");
+ }
+ }
+
+ private Stream<String> newProjectsNamesStream(String query)
+ throws OrmException, MethodNotAllowedException, BadRequestException {
+ Stream<String> projects =
+ queryProjectsProvider.get().withQuery(query).apply().stream().map(p -> p.name).skip(start);
+ if (limit > 0) {
+ projects = projects.limit(limit);
+ }
+
+ return projects;
+ }
+
+ public void displayToStream(OutputStream displayOutputStream)
+ throws BadRequestException, PermissionBackendException {
+ PrintWriter stdout =
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8)));
+ Optional<String> projectsQuery = expressAsProjectsQuery();
+
+ if (projectsQuery.isPresent()) {
+ printQueryResults(projectsQuery.get(), stdout);
+ } else {
+ display(stdout);
+ }
+ }
+
+ @Nullable
+ public SortedMap<String, ProjectInfo> display(@Nullable PrintWriter stdout)
throws BadRequestException, PermissionBackendException {
if (all && state != null) {
throw new BadRequestException("'all' and 'state' may not be used together");
@@ -320,12 +424,6 @@
}
}
- PrintWriter stdout = null;
- if (displayOutputStream != null) {
- stdout =
- new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8)));
- }
-
int foundIndex = 0;
int found = 0;
TreeMap<String, ProjectInfo> output = new TreeMap<>();
@@ -373,7 +471,7 @@
}
if (showDescription) {
- info.description = Strings.emptyToNull(e.getProject().getDescription());
+ info.description = emptyToNull(e.getProject().getDescription());
}
info.state = e.getProject().getState();
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index 7a46b9c..98c8c61 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -226,9 +226,6 @@
return permissionBackend
.currentUser()
.project(project)
- .filter(
- tags,
- repo,
- RefFilterOptions.builder().setFilterMeta(true).setFilterTagsSeparately(true).build());
+ .filter(tags, repo, RefFilterOptions.builder().setFilterMeta(true).build());
}
}
diff --git a/java/com/google/gerrit/server/restapi/project/QueryProjects.java b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
index 1e094a0..44432aa 100644
--- a/java/com/google/gerrit/server/restapi/project/QueryProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/QueryProjects.java
@@ -49,8 +49,9 @@
name = "--query",
aliases = {"-q"},
usage = "project query")
- public void setQuery(String query) {
+ public QueryProjects withQuery(String query) {
this.query = query;
+ return this;
}
@Option(
@@ -58,8 +59,9 @@
aliases = {"-n"},
metaVar = "CNT",
usage = "maximum number of projects to list")
- public void setLimit(int limit) {
+ public QueryProjects withLimit(int limit) {
this.limit = limit;
+ return this;
}
@Option(
@@ -67,8 +69,9 @@
aliases = {"-S"},
metaVar = "CNT",
usage = "number of projects to skip")
- public void setStart(int start) {
+ public QueryProjects withStart(int start) {
this.start = start;
+ return this;
}
@Inject
@@ -86,6 +89,11 @@
@Override
public List<ProjectInfo> apply(TopLevelResource resource)
throws BadRequestException, MethodNotAllowedException, OrmException {
+ return apply();
+ }
+
+ public List<ProjectInfo> apply()
+ throws BadRequestException, MethodNotAllowedException, OrmException {
if (Strings.isNullOrEmpty(query)) {
throw new BadRequestException("missing query field");
}
diff --git a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
index b9ddbc6..4a7eea7 100644
--- a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
+++ b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
@@ -71,7 +71,7 @@
return singletonRuleError(E_UNABLE_TO_FETCH_LABELS);
}
- boolean shouldIgnoreSelfApproval = labelTypes.stream().anyMatch(l -> l.ignoreSelfApproval());
+ boolean shouldIgnoreSelfApproval = labelTypes.stream().anyMatch(LabelType::ignoreSelfApproval);
if (!shouldIgnoreSelfApproval) {
// Shortcut to avoid further processing if no label should ignore uploader approvals
return ImmutableList.of();
diff --git a/java/com/google/gerrit/server/rules/PrologEnvironment.java b/java/com/google/gerrit/server/rules/PrologEnvironment.java
index 412e0f9..e7fc4db 100644
--- a/java/com/google/gerrit/server/rules/PrologEnvironment.java
+++ b/java/com/google/gerrit/server/rules/PrologEnvironment.java
@@ -20,6 +20,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -170,6 +171,7 @@
private final ProjectCache projectCache;
private final PermissionBackend permissionBackend;
private final GitRepositoryManager repositoryManager;
+ private final PluginConfigFactory pluginConfigFactory;
private final PatchListCache patchListCache;
private final PatchSetInfoFactory patchSetInfoFactory;
private final IdentifiedUser.GenericFactory userFactory;
@@ -184,6 +186,7 @@
ProjectCache projectCache,
PermissionBackend permissionBackend,
GitRepositoryManager repositoryManager,
+ PluginConfigFactory pluginConfigFactory,
PatchListCache patchListCache,
PatchSetInfoFactory patchSetInfoFactory,
IdentifiedUser.GenericFactory userFactory,
@@ -194,6 +197,7 @@
this.projectCache = projectCache;
this.permissionBackend = permissionBackend;
this.repositoryManager = repositoryManager;
+ this.pluginConfigFactory = pluginConfigFactory;
this.patchListCache = patchListCache;
this.patchSetInfoFactory = patchSetInfoFactory;
this.userFactory = userFactory;
@@ -232,6 +236,10 @@
return repositoryManager;
}
+ public PluginConfigFactory getPluginConfigFactory() {
+ return pluginConfigFactory;
+ }
+
public PatchListCache getPatchListCache() {
return patchListCache;
}
diff --git a/java/com/google/gerrit/server/rules/StoredValues.java b/java/com/google/gerrit/server/rules/StoredValues.java
index aa529d7..1c28c54 100644
--- a/java/com/google/gerrit/server/rules/StoredValues.java
+++ b/java/com/google/gerrit/server/rules/StoredValues.java
@@ -27,6 +27,7 @@
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
@@ -120,6 +121,15 @@
}
};
+ public static final StoredValue<PluginConfigFactory> PLUGIN_CONFIG_FACTORY =
+ new StoredValue<PluginConfigFactory>() {
+ @Override
+ public PluginConfigFactory createValue(Prolog engine) {
+ PrologEnvironment env = (PrologEnvironment) engine.control;
+ return env.getArgs().getPluginConfigFactory();
+ }
+ };
+
public static final StoredValue<Repository> REPOSITORY =
new StoredValue<Repository>() {
@Override
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java b/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
index 281f0ab..f8a9cf7 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaUpdater.java
@@ -91,7 +91,7 @@
for (int nextVersion : requiredUpgrades(currentVersion, schemaVersions.keySet())) {
try {
ui.message(String.format("Migrating data to schema %d ...", nextVersion));
- NoteDbSchemaVersions.get(schemaVersions, nextVersion, args).upgrade(ui);
+ NoteDbSchemaVersions.get(schemaVersions, nextVersion).upgrade(args, ui);
versionManager.increment(nextVersion - 1);
} catch (Exception e) {
throw new OrmException(
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersion.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersion.java
index e90b2b8..75a1de2 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaVersion.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersion.java
@@ -22,9 +22,8 @@
/**
* Schema upgrade implementation.
*
- * <p>Implementations must define a single public constructor that takes an {@link Arguments}. The
- * recommended idiom is to pull out whichever individual fields from the {@code Arguments} are
- * required by this implementation.
+ * <p>Implementations must have a single non-private constructor with no arguments (e.g. the default
+ * constructor).
*/
interface NoteDbSchemaVersion {
@Singleton
@@ -39,5 +38,5 @@
}
}
- void upgrade(UpdateUI ui) throws Exception;
+ void upgrade(Arguments args, UpdateUI ui) throws Exception;
}
diff --git a/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java b/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java
index ffd4760..3556ec5 100644
--- a/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java
+++ b/java/com/google/gerrit/server/schema/NoteDbSchemaVersions.java
@@ -45,13 +45,11 @@
}
public static NoteDbSchemaVersion get(
- ImmutableSortedMap<Integer, Class<? extends NoteDbSchemaVersion>> schemaVersions,
- int i,
- NoteDbSchemaVersion.Arguments args) {
+ ImmutableSortedMap<Integer, Class<? extends NoteDbSchemaVersion>> schemaVersions, int i) {
Class<? extends NoteDbSchemaVersion> clazz = schemaVersions.get(i);
checkArgument(clazz != null, "Schema version not found: %s", i);
try {
- return clazz.getDeclaredConstructor(NoteDbSchemaVersion.Arguments.class).newInstance(args);
+ return clazz.getDeclaredConstructor().newInstance();
} catch (InstantiationException
| IllegalAccessException
| NoSuchMethodException
diff --git a/java/com/google/gerrit/server/schema/Schema_180.java b/java/com/google/gerrit/server/schema/Schema_180.java
index 4d16022..6912b3e 100644
--- a/java/com/google/gerrit/server/schema/Schema_180.java
+++ b/java/com/google/gerrit/server/schema/Schema_180.java
@@ -15,13 +15,8 @@
package com.google.gerrit.server.schema;
public class Schema_180 implements NoteDbSchemaVersion {
- @SuppressWarnings("unused")
- Schema_180(Arguments args) {
- // Do nothing.
- }
-
@Override
- public void upgrade(UpdateUI ui) {
+ public void upgrade(Arguments args, UpdateUI ui) {
// Do nothing; only used to populate the version ref, which is done by the caller.
}
}
diff --git a/java/com/google/gerrit/server/submit/EmailMerge.java b/java/com/google/gerrit/server/submit/EmailMerge.java
index eabbe00..a1f56eb 100644
--- a/java/com/google/gerrit/server/submit/EmailMerge.java
+++ b/java/com/google/gerrit/server/submit/EmailMerge.java
@@ -14,16 +14,14 @@
package com.google.gerrit.server.submit;
-import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.config.SendEmailExecutor;
import com.google.gerrit.server.mail.send.MergedSender;
import com.google.gerrit.server.util.RequestContext;
@@ -42,8 +40,7 @@
Project.NameKey project,
Change.Id changeId,
Account.Id submitter,
- NotifyHandling notifyHandling,
- ListMultimap<RecipientType, Account.Id> accountsToNotify);
+ NotifyResolver.Result notify);
}
private final ExecutorService sendEmailsExecutor;
@@ -54,8 +51,7 @@
private final Project.NameKey project;
private final Change.Id changeId;
private final Account.Id submitter;
- private final NotifyHandling notifyHandling;
- private final ListMultimap<RecipientType, Account.Id> accountsToNotify;
+ private final NotifyResolver.Result notify;
@Inject
EmailMerge(
@@ -66,8 +62,7 @@
@Assisted Project.NameKey project,
@Assisted Change.Id changeId,
@Assisted @Nullable Account.Id submitter,
- @Assisted NotifyHandling notifyHandling,
- @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify) {
+ @Assisted NotifyResolver.Result notify) {
this.sendEmailsExecutor = executor;
this.mergedSenderFactory = mergedSenderFactory;
this.requestContext = requestContext;
@@ -75,8 +70,7 @@
this.project = project;
this.changeId = changeId;
this.submitter = submitter;
- this.notifyHandling = notifyHandling;
- this.accountsToNotify = accountsToNotify;
+ this.notify = notify;
}
void sendAsync() {
@@ -92,8 +86,7 @@
if (submitter != null) {
cm.setFrom(submitter);
}
- cm.setNotify(notifyHandling);
- cm.setAccountsToNotify(accountsToNotify);
+ cm.setNotify(notify);
cm.send();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email merged notification for %s", changeId);
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 5b25444..3936096 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.submit;
+import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Comparator.comparing;
@@ -35,7 +36,7 @@
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.extensions.api.changes.RecipientType;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -46,7 +47,6 @@
import com.google.gerrit.metrics.Counter0;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.MetricMaker;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -55,7 +55,7 @@
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.InternalUser;
-import com.google.gerrit.server.change.NotifyUtil;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.MergeTip;
import com.google.gerrit.server.git.validators.MergeValidationException;
@@ -228,7 +228,7 @@
private final SubmitStrategyFactory submitStrategyFactory;
private final SubmoduleOp.Factory subOpFactory;
private final Provider<MergeOpRepoManager> ormProvider;
- private final NotifyUtil notifyUtil;
+ private final NotifyResolver notifyResolver;
private final RetryHelper retryHelper;
private final ChangeData.Factory changeDataFactory;
@@ -239,7 +239,7 @@
private MergeOpRepoManager orm;
private CommitStatus commitStatus;
private SubmitInput submitInput;
- private ListMultimap<RecipientType, Account.Id> accountsToNotify;
+ private NotifyResolver.Result notify;
private Set<Project.NameKey> allProjects;
private boolean dryrun;
private TopicMetrics topicMetrics;
@@ -255,7 +255,7 @@
SubmitStrategyFactory submitStrategyFactory,
SubmoduleOp.Factory subOpFactory,
Provider<MergeOpRepoManager> ormProvider,
- NotifyUtil notifyUtil,
+ NotifyResolver notifyResolver,
TopicMetrics topicMetrics,
RetryHelper retryHelper,
ChangeData.Factory changeDataFactory) {
@@ -268,7 +268,7 @@
this.submitStrategyFactory = submitStrategyFactory;
this.subOpFactory = subOpFactory;
this.ormProvider = ormProvider;
- this.notifyUtil = notifyUtil;
+ this.notifyResolver = notifyResolver;
this.retryHelper = retryHelper;
this.topicMetrics = topicMetrics;
this.changeDataFactory = changeDataFactory;
@@ -443,7 +443,9 @@
throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException,
PermissionBackendException {
this.submitInput = submitInput;
- this.accountsToNotify = notifyUtil.resolveAccounts(submitInput.notifyDetails);
+ this.notify =
+ notifyResolver.resolve(
+ firstNonNull(submitInput.notify, NotifyHandling.ALL), submitInput.notifyDetails);
this.dryrun = dryrun;
this.caller = caller;
this.ts = TimeUtil.nowTs();
@@ -531,7 +533,7 @@
orm.close();
}
orm = ormProvider.get();
- orm.setContext(ts, caller);
+ orm.setContext(ts, caller, notify);
}
private ChangeSet reloadChanges(ChangeSet changeSet) {
@@ -674,7 +676,6 @@
commitStatus,
submissionId,
submitInput,
- accountsToNotify,
submoduleOp,
dryrun);
strategies.add(strategy);
diff --git a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
index cbaad6a..764aca8 100644
--- a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
+++ b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
@@ -15,12 +15,14 @@
package com.google.gerrit.server.submit;
import static com.google.common.base.Preconditions.checkState;
+import static java.util.Objects.requireNonNull;
import com.google.common.collect.Maps;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -110,6 +112,7 @@
batchUpdateFactory
.create(getProjectName(), caller, ts)
.setRepository(repo, rw, ins)
+ .setNotify(notify)
.setOnSubmitValidators(onSubmitValidatorsFactory.create());
}
return update;
@@ -158,6 +161,7 @@
private Timestamp ts;
private IdentifiedUser caller;
+ private NotifyResolver.Result notify;
@Inject
MergeOpRepoManager(
@@ -173,9 +177,10 @@
openRepos = new HashMap<>();
}
- public void setContext(Timestamp ts, IdentifiedUser caller) {
- this.ts = ts;
- this.caller = caller;
+ public void setContext(Timestamp ts, IdentifiedUser caller, NotifyResolver.Result notify) {
+ this.ts = requireNonNull(ts);
+ this.caller = requireNonNull(caller);
+ this.notify = requireNonNull(notify);
}
public OpenRepo getRepo(Project.NameKey project) throws NoSuchProjectException, IOException {
@@ -200,7 +205,7 @@
throws NoSuchProjectException, IOException {
List<BatchUpdate> updates = new ArrayList<>(projects.size());
for (Project.NameKey project : projects) {
- updates.add(getRepo(project).getUpdate().setRefLogMessage("merged"));
+ updates.add(getRepo(project).getUpdate().setNotify(notify).setRefLogMessage("merged"));
}
return updates;
}
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategy.java b/java/com/google/gerrit/server/submit/SubmitStrategy.java
index c9baf99..37858d5 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategy.java
@@ -16,13 +16,10 @@
import static java.util.Objects.requireNonNull;
-import com.google.common.collect.ListMultimap;
import com.google.common.collect.Sets;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.config.FactoryModule;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.ApprovalsUtil;
@@ -94,7 +91,6 @@
Set<CodeReviewCommit> incoming,
RequestId submissionId,
SubmitInput submitInput,
- ListMultimap<RecipientType, Account.Id> accountsToNotify,
SubmoduleOp submoduleOp,
boolean dryrun);
}
@@ -126,7 +122,6 @@
final RequestId submissionId;
final SubmitType submitType;
final SubmitInput submitInput;
- final ListMultimap<RecipientType, Account.Id> accountsToNotify;
final SubmoduleOp submoduleOp;
final ProjectState project;
@@ -165,7 +160,6 @@
@Assisted RequestId submissionId,
@Assisted SubmitType submitType,
@Assisted SubmitInput submitInput,
- @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify,
@Assisted SubmoduleOp submoduleOp,
@Assisted boolean dryrun) {
this.accountCache = accountCache;
@@ -194,7 +188,6 @@
this.submissionId = submissionId;
this.submitType = submitType;
this.submitInput = submitInput;
- this.accountsToNotify = accountsToNotify;
this.submoduleOp = submoduleOp;
this.dryrun = dryrun;
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
index 0a92d61..30326f7 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyFactory.java
@@ -14,12 +14,9 @@
package com.google.gerrit.server.submit;
-import com.google.common.collect.ListMultimap;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.SubmitType;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.CodeReviewCommit;
@@ -57,7 +54,6 @@
CommitStatus commitStatus,
RequestId submissionId,
SubmitInput submitInput,
- ListMultimap<RecipientType, Account.Id> accountsToNotify,
SubmoduleOp submoduleOp,
boolean dryrun)
throws IntegrationException {
@@ -74,7 +70,6 @@
incoming,
submissionId,
submitInput,
- accountsToNotify,
submoduleOp,
dryrun);
switch (submitType) {
diff --git a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
index 8a6f914..a49ddff 100644
--- a/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
+++ b/java/com/google/gerrit/server/submit/SubmitStrategyOp.java
@@ -501,12 +501,7 @@
// have failed fast in one of the other steps.
try {
args.mergedSenderFactory
- .create(
- ctx.getProject(),
- getId(),
- submitter.getAccountId(),
- args.submitInput.notify,
- args.accountsToNotify)
+ .create(ctx.getProject(), getId(), submitter.getAccountId(), ctx.getNotify(getId()))
.sendAsync();
} catch (Exception e) {
logger.atSevere().withCause(e).log("Cannot email merged notification for %s", getId());
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 06397d7..8cf302b 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -30,6 +30,7 @@
import com.google.common.collect.Multiset;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -40,6 +41,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.validators.OnSubmitValidators;
@@ -229,6 +231,12 @@
public CurrentUser getUser() {
return user;
}
+
+ @Override
+ public NotifyResolver.Result getNotify(Change.Id changeId) {
+ NotifyHandling notifyHandling = perChangeNotifyHandling.get(changeId);
+ return notifyHandling != null ? notify.withHandling(notifyHandling) : notify;
+ }
}
private class RepoContextImpl extends ContextImpl implements RepoContext {
@@ -302,12 +310,14 @@
MultimapBuilder.linkedHashKeys().arrayListValues().build();
private final Map<Change.Id, Change> newChanges = new HashMap<>();
private final List<RepoOnlyOp> repoOnlyOps = new ArrayList<>();
+ private final Map<Change.Id, NotifyHandling> perChangeNotifyHandling = new HashMap<>();
private RepoView repoView;
private BatchRefUpdate batchRefUpdate;
private OnSubmitValidators onSubmitValidators;
private PushCertificate pushCert;
private String refLogMessage;
+ private NotifyResolver.Result notify = NotifyResolver.Result.all();
@Inject
BatchUpdate(
@@ -365,6 +375,32 @@
}
/**
+ * Set the default notification settings for all changes in the batch.
+ *
+ * @param notify notification settings.
+ * @return this.
+ */
+ public BatchUpdate setNotify(NotifyResolver.Result notify) {
+ this.notify = requireNonNull(notify);
+ return this;
+ }
+
+ /**
+ * Override the {@link NotifyHandling} on a per-change basis.
+ *
+ * <p>Only the handling enum can be overridden; all changes share the same value for {@link
+ * com.google.gerrit.server.change.NotifyResolver.Result#accounts()}.
+ *
+ * @param changeId change ID.
+ * @param notifyHandling notify handling.
+ * @return this.
+ */
+ public BatchUpdate setNotifyHandling(Change.Id changeId, NotifyHandling notifyHandling) {
+ this.perChangeNotifyHandling.put(changeId, requireNonNull(notifyHandling));
+ return this;
+ }
+
+ /**
* Add a validation step for intended ref operations, which will be performed at the end of {@link
* RepoOnlyOp#updateRepo(RepoContext)} step.
*/
diff --git a/java/com/google/gerrit/server/update/Context.java b/java/com/google/gerrit/server/update/Context.java
index c24c650..8704cf0 100644
--- a/java/com/google/gerrit/server/update/Context.java
+++ b/java/com/google/gerrit/server/update/Context.java
@@ -17,10 +17,12 @@
import static java.util.Objects.requireNonNull;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.change.NotifyResolver;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.TimeZone;
@@ -86,6 +88,18 @@
CurrentUser getUser();
/**
+ * Get the notification settings configured by the caller.
+ *
+ * <p>If there are multiple changes in a batch, they may have different settings. For example, WIP
+ * changes may have reduced {@code NotifyHandling} levels, and may be in a batch with non-WIP
+ * changes.
+ *
+ * @param changeId change ID
+ * @return notification settings.
+ */
+ NotifyResolver.Result getNotify(Change.Id changeId);
+
+ /**
* Get the identified user performing the update.
*
* <p>Convenience method for {@code getUser().asIdentifiedUser()}.
diff --git a/java/com/google/gerrit/sshd/SshLog.java b/java/com/google/gerrit/sshd/SshLog.java
index df3242c..5fb75c8 100644
--- a/java/com/google/gerrit/sshd/SshLog.java
+++ b/java/com/google/gerrit/sshd/SshLog.java
@@ -21,7 +21,6 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PeerDaemonUser;
-import com.google.gerrit.server.audit.AuditService;
import com.google.gerrit.server.audit.SshAuditEvent;
import com.google.gerrit.server.config.ConfigKey;
import com.google.gerrit.server.config.ConfigUpdatedEvent;
@@ -29,6 +28,7 @@
import com.google.gerrit.server.config.ConfigUpdatedEvent.UpdateResult;
import com.google.gerrit.server.config.GerritConfigListener;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.GroupAuditService;
import com.google.gerrit.server.ioutil.HexFormat;
import com.google.gerrit.server.util.SystemLog;
import com.google.gerrit.server.util.time.TimeUtil;
@@ -57,7 +57,7 @@
private final Provider<SshSession> session;
private final Provider<Context> context;
private volatile AsyncAppender async;
- private final AuditService auditService;
+ private final GroupAuditService auditService;
private final SystemLog systemLog;
private final Object lock = new Object();
@@ -68,7 +68,7 @@
final Provider<Context> context,
SystemLog systemLog,
@GerritServerConfig Config config,
- AuditService auditService) {
+ GroupAuditService auditService) {
this.session = session;
this.context = context;
this.auditService = auditService;
diff --git a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index d04e2d3..9f2ffa9 100644
--- a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -41,6 +41,6 @@
throw die("--tree and --description options are not compatible.");
}
}
- impl.display(out);
+ impl.displayToStream(out);
}
}
diff --git a/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
index 969ce50..3d9ef565 100644
--- a/java/com/google/gerrit/sshd/commands/LsUserRefs.java
+++ b/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -18,6 +18,7 @@
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
@@ -73,21 +74,20 @@
@Override
protected void run() throws Failure {
- Account userAccount;
+ Account.Id userAccountId;
try {
- userAccount = accountResolver.find(userName);
- } catch (OrmException | IOException | ConfigInvalidException e) {
- throw die(e);
- }
- if (userAccount == null) {
- stdout.print("No single user could be found when searching for: " + userName + '\n');
+ userAccountId = accountResolver.resolve(userName).asUnique().getAccount().getId();
+ } catch (UnprocessableEntityException e) {
+ stdout.println(e.getMessage());
stdout.flush();
return;
+ } catch (OrmException | IOException | ConfigInvalidException e) {
+ throw die(e);
}
Project.NameKey projectName = projectState.getNameKey();
try (Repository repo = repoManager.openRepository(projectName);
- ManualRequestContext ctx = requestContext.openAs(userAccount.getId())) {
+ ManualRequestContext ctx = requestContext.openAs(userAccountId)) {
try {
Map<String, Ref> refsMap =
permissionBackend
diff --git a/java/com/google/gerrit/sshd/commands/SetParentCommand.java b/java/com/google/gerrit/sshd/commands/SetParentCommand.java
index 56ee371..f2d8c4c 100644
--- a/java/com/google/gerrit/sshd/commands/SetParentCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetParentCommand.java
@@ -33,6 +33,7 @@
import com.google.gerrit.server.restapi.project.SetParent;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
+import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
@@ -111,7 +112,7 @@
childProjects.addAll(getChildrenForReparenting(oldParent));
} catch (PermissionBackendException e) {
throw new Failure(1, "permissions unavailable", e);
- } catch (RestApiException e) {
+ } catch (OrmException | RestApiException e) {
throw new Failure(1, "failure in request", e);
}
}
@@ -148,7 +149,7 @@
* reparenting.
*/
private List<Project.NameKey> getChildrenForReparenting(ProjectState parent)
- throws PermissionBackendException, RestApiException {
+ throws PermissionBackendException, OrmException, RestApiException {
final List<Project.NameKey> childProjects = new ArrayList<>();
final List<Project.NameKey> excluded = new ArrayList<>(excludedChildren.size());
for (ProjectState excludedChild : excludedChildren) {
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index f7eb342..06f7dbd 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -516,12 +516,27 @@
@Test
public void active() throws Exception {
+ int id = gApi.accounts().id("user").get()._accountId;
assertThat(gApi.accounts().id("user").getActive()).isTrue();
gApi.accounts().id("user").setActive(false);
- assertThat(gApi.accounts().id("user").getActive()).isFalse();
accountIndexedCounter.assertReindexOf(user);
- gApi.accounts().id("user").setActive(true);
+ // Inactive users may only be resolved by ID.
+ try {
+ gApi.accounts().id("user");
+ assert_().fail("expected ResourceNotFoundException");
+ } catch (ResourceNotFoundException e) {
+ assertThat(e)
+ .hasMessageThat()
+ .isEqualTo(
+ "Account 'user' only matches inactive accounts. To use an inactive account, retry"
+ + " with one of the following exact account IDs:\n"
+ + id
+ + ": User <user@example.com>");
+ }
+ assertThat(gApi.accounts().id(id).getActive()).isFalse();
+
+ gApi.accounts().id(id).setActive(true);
assertThat(gApi.accounts().id("user").getActive()).isTrue();
accountIndexedCounter.assertReindexOf(user);
}
@@ -620,16 +635,17 @@
@Test
public void deactivateNotActive() throws Exception {
+ int id = gApi.accounts().id("user").get()._accountId;
assertThat(gApi.accounts().id("user").getActive()).isTrue();
gApi.accounts().id("user").setActive(false);
- assertThat(gApi.accounts().id("user").getActive()).isFalse();
+ assertThat(gApi.accounts().id(id).getActive()).isFalse();
try {
- gApi.accounts().id("user").setActive(false);
+ gApi.accounts().id(id).setActive(false);
fail("Expected exception");
} catch (ResourceConflictException e) {
assertThat(e.getMessage()).isEqualTo("account not active");
}
- gApi.accounts().id("user").setActive(true);
+ gApi.accounts().id(id).setActive(true);
}
@Test
@@ -2121,7 +2137,7 @@
TestAccount foo2 = accountCreator.create(name + "-2");
gApi.accounts().id(foo2.username).setActive(false);
- assertThat(gApi.accounts().id(foo2.username).getActive()).isFalse();
+ assertThat(gApi.accounts().id(foo2.id.get()).getActive()).isFalse();
assertThat(accountQueryProvider.get().byDefault(name)).hasSize(2);
}
@@ -2257,7 +2273,7 @@
new AccountsUpdate(
repoManager,
gitReferenceUpdated,
- null,
+ Optional.empty(),
allUsers,
externalIds,
metaDataUpdateInternalFactory,
@@ -2307,7 +2323,7 @@
new AccountsUpdate(
repoManager,
gitReferenceUpdated,
- null,
+ Optional.empty(),
allUsers,
externalIds,
metaDataUpdateInternalFactory,
@@ -2367,7 +2383,7 @@
new AccountsUpdate(
repoManager,
gitReferenceUpdated,
- null,
+ Optional.empty(),
allUsers,
externalIds,
metaDataUpdateInternalFactory,
@@ -2433,7 +2449,7 @@
new AccountsUpdate(
repoManager,
gitReferenceUpdated,
- null,
+ Optional.empty(),
allUsers,
externalIds,
metaDataUpdateInternalFactory,
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 04e23ae..21f8428 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -86,6 +86,7 @@
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.ReviewResult;
+import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.changes.StarsInput;
import com.google.gerrit.extensions.api.groups.GroupApi;
@@ -1676,18 +1677,48 @@
PushOneCommit.Result result = createChange();
String username = name("new-user");
- accountOperations.newAccount().username(username).inactive().create();
+ Account.Id id = accountOperations.newAccount().username(username).inactive().create();
AddReviewerInput in = new AddReviewerInput();
in.reviewer = username;
AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
- assertThat(r.input).isEqualTo(username);
- assertThat(r.error).contains("identifies an inactive account");
+ assertThat(r.input).isEqualTo(in.reviewer);
+ assertThat(r.error)
+ .isEqualTo(
+ "Account '"
+ + username
+ + "' only matches inactive accounts. To use an inactive account, retry with one of"
+ + " the following exact account IDs:\n"
+ + id
+ + ": Name of user not set ("
+ + id
+ + ")\n"
+ + username
+ + " does not identify a registered user or group");
assertThat(r.reviewers).isNull();
}
@Test
+ public void addReviewerThatIsInactiveById() throws Exception {
+ PushOneCommit.Result result = createChange();
+
+ String username = name("new-user");
+ Account.Id id = accountOperations.newAccount().username(username).inactive().create();
+
+ AddReviewerInput in = new AddReviewerInput();
+ in.reviewer = Integer.toString(id.get());
+ AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
+
+ assertThat(r.input).isEqualTo(in.reviewer);
+ assertThat(r.error).isNull();
+ assertThat(r.reviewers).hasSize(1);
+ ReviewerInfo reviewer = r.reviewers.get(0);
+ assertThat(reviewer._accountId).isEqualTo(id.get());
+ assertThat(reviewer.username).isEqualTo(username);
+ }
+
+ @Test
public void addReviewerThatIsInactiveEmailFallback() throws Exception {
ConfigInput conf = new ConfigInput();
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
@@ -2451,7 +2482,7 @@
in.project = project.get();
ChangeInfo info = gApi.changes().create(in).get();
assertThat(info.project).isEqualTo(in.project);
- assertThat(info.branch).isEqualTo(in.branch);
+ assertThat(RefNames.fullName(info.branch)).isEqualTo(RefNames.fullName(in.branch));
assertThat(info.subject).isEqualTo(in.subject);
assertThat(Iterables.getOnlyElement(info.messages).message).isEqualTo("Uploaded patch set 1.");
}
@@ -2899,7 +2930,7 @@
in.newBranch = true;
ChangeInfo info = gApi.changes().create(in).get();
assertThat(info.project).isEqualTo(in.project);
- assertThat(info.branch).isEqualTo(in.branch);
+ assertThat(RefNames.fullName(info.branch)).isEqualTo(RefNames.fullName(in.branch));
assertThat(info.subject).isEqualTo(in.subject);
assertThat(Iterables.getOnlyElement(info.messages).message).isEqualTo("Uploaded patch set 1.");
}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
index f0296fc..30a8b6b 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/CheckAccessIT.java
@@ -132,7 +132,7 @@
in.ref = "refs/heads/master";
exception.expect(UnprocessableEntityException.class);
- exception.expectMessage("cannot find account doesnotexist@invalid.com");
+ exception.expectMessage("Account 'doesnotexist@invalid.com' not found");
gApi.projects().name(normalProject.get()).checkAccess(in);
}
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 3507714..e8c828a 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -35,6 +35,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
+import com.google.common.collect.ListMultimap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
@@ -1460,6 +1461,47 @@
.containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId()));
}
+ @Test
+ public void listVotesByRevision() throws Exception {
+ // Create patch set 1 and vote on it
+ String changeId = createChange().getChangeId();
+ ListMultimap<String, ApprovalInfo> votes = gApi.changes().id(changeId).current().votes();
+ assertThat(votes).isEmpty();
+ recommend(changeId);
+ votes = gApi.changes().id(changeId).current().votes();
+ assertThat(votes.keySet()).containsExactly("Code-Review");
+ List<ApprovalInfo> approvals = votes.get("Code-Review");
+ assertThat(approvals).hasSize(1);
+ ApprovalInfo approval = approvals.get(0);
+ assertThat(approval._accountId).isEqualTo(admin.id.get());
+ assertThat(approval.email).isEqualTo(admin.email);
+ assertThat(approval.username).isEqualTo(admin.username);
+
+ // Also vote on it with another user
+ requestScopeOperations.setApiUser(user.getId());
+ gApi.changes().id(changeId).current().review(ReviewInput.dislike());
+
+ // Patch set 1 has 2 votes on Code-Review
+ requestScopeOperations.setApiUser(admin.getId());
+ votes = gApi.changes().id(changeId).current().votes();
+ assertThat(votes.keySet()).containsExactly("Code-Review");
+ approvals = votes.get("Code-Review");
+ assertThat(approvals).hasSize(2);
+ assertThat(approvals.stream().map(a -> a._accountId))
+ .containsExactlyElementsIn(ImmutableList.of(admin.id.get(), user.id.get()));
+
+ // Create a new patch set which does not have any votes
+ amendChange(changeId);
+ votes = gApi.changes().id(changeId).current().votes();
+ assertThat(votes).isEmpty();
+
+ // Votes are still returned for ps 1
+ votes = gApi.changes().id(changeId).revision(1).votes();
+ assertThat(votes.keySet()).containsExactly("Code-Review");
+ approvals = votes.get("Code-Review");
+ assertThat(approvals).hasSize(2);
+ }
+
private static void assertCherryPickResult(
ChangeInfo changeInfo, CherryPickInput input, String srcChangeId) throws Exception {
assertThat(changeInfo.changeId).isEqualTo(srcChangeId);
diff --git a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 427747a..ce8327b 100644
--- a/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/javatests/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -24,6 +24,7 @@
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.NoHttpd;
@@ -351,36 +352,32 @@
allow("refs/heads/*", Permission.READ, REGISTERED_USERS);
requestScopeOperations.setApiUser(user.getId());
- try (Repository repo = repoManager.openRepository(project)) {
- assertRefs(
- repo,
- permissionBackend.user(user(user)).project(project),
- // Can't use stored values from the index so DB must be enabled.
- false,
- "HEAD",
- psRef1,
- metaRef1,
- psRef2,
- metaRef2,
- psRef3,
- metaRef3,
- psRef4,
- metaRef4,
- "refs/heads/branch",
- "refs/heads/master",
- "refs/tags/branch-tag",
- "refs/tags/master-tag");
- }
+ assertRefs(
+ project,
+ user,
+ // Can't use stored values from the index so DB must be enabled.
+ false,
+ "HEAD",
+ psRef1,
+ metaRef1,
+ psRef2,
+ metaRef2,
+ psRef3,
+ metaRef3,
+ psRef4,
+ metaRef4,
+ "refs/heads/branch",
+ "refs/heads/master",
+ "refs/tags/branch-tag",
+ "refs/tags/master-tag");
}
@Test
public void uploadPackSequencesWithAccessDatabase() throws Exception {
- try (Repository repo = repoManager.openRepository(allProjects)) {
- assertRefs(repo, newFilter(allProjects, user), true);
+ assertRefs(allProjects, user, true);
- allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
- assertRefs(repo, newFilter(allProjects, user), true, "refs/sequences/changes");
- }
+ allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+ assertRefs(allProjects, user, true, "refs/sequences/changes");
}
@Test
@@ -696,6 +693,22 @@
}
}
+ @Test
+ public void fetchSingleChangeWithoutIndexAccess() throws Exception {
+ PushOneCommit.Result change = createChange();
+ String patchSetRef = change.getPatchSetId().toRefName();
+ try (AutoCloseable ignored = disableChangeIndex();
+ Repository repo = repoManager.openRepository(project)) {
+ Map<String, Ref> singleRef = ImmutableMap.of(patchSetRef, repo.exactRef(patchSetRef));
+ Map<String, Ref> filteredRefs =
+ permissionBackend
+ .user(user(admin))
+ .project(project)
+ .filter(singleRef, repo, RefFilterOptions.defaults());
+ assertThat(filteredRefs).isEqualTo(singleRef);
+ }
+ }
+
private List<String> lsRemote(Project.NameKey p, TestAccount a) throws Exception {
TestRepository<?> testRepository = cloneProject(p, a);
try (Git git = testRepository.git()) {
@@ -726,31 +739,18 @@
* @throws Exception
*/
private void assertUploadPackRefs(String... expectedRefs) throws Exception {
- try (Repository repo = repoManager.openRepository(project)) {
- assertRefs(repo, permissionBackend.user(user(user)).project(project), true, expectedRefs);
- }
+ assertRefs(project, user, true, expectedRefs);
}
private void assertRefs(
- Repository repo,
- PermissionBackend.ForProject forProject,
- boolean disableDb,
- String... expectedRefs)
+ Project.NameKey project, TestAccount user, boolean disableDb, String... expectedRefs)
throws Exception {
AutoCloseable ctx = null;
if (disableDb) {
ctx = disableNoteDb();
}
try {
- Map<String, Ref> all = getAllRefs(repo);
- assertThat(
- forProject
- .filter(
- all,
- repo,
- RefFilterOptions.defaults().toBuilder().setFilterTagsSeparately(true).build())
- .keySet())
- .containsExactlyElementsIn(expectedRefs);
+ assertThat(lsRemote(project, user)).containsExactlyElementsIn(expectedRefs);
} finally {
if (disableDb) {
ctx.close();
diff --git a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
index bb7cff7..af71ebd 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/ElasticReindexIT.java
@@ -32,7 +32,7 @@
@ConfigSuite.Config
public static Config elasticsearchV6() {
- return getConfig(ElasticVersion.V6_5);
+ return getConfig(ElasticVersion.V6_6);
}
@ConfigSuite.Config
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
index 621a52e..0b39f0a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance.rest.change;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -29,8 +30,8 @@
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.gerrit.testing.TestTimeUtil;
import com.google.inject.Inject;
@@ -129,9 +130,28 @@
public void setAssigneeToInactiveUser() throws Exception {
PushOneCommit.Result r = createChange();
gApi.accounts().id(user.getId().get()).setActive(false);
- exception.expect(UnprocessableEntityException.class);
- exception.expectMessage("is not active");
- setAssignee(r, user.email);
+ try {
+ setAssignee(r, user.email);
+ assert_().fail("expected UnresolvableAccountException");
+ } catch (UnresolvableAccountException e) {
+ assertThat(e)
+ .hasMessageThat()
+ .isEqualTo(
+ "Account '"
+ + user.email
+ + "' only matches inactive accounts. To use an inactive account, retry with one"
+ + " of the following exact account IDs:\n"
+ + user.id
+ + ": User <user@example.com>");
+ }
+ }
+
+ @Test
+ public void setAssigneeToInactiveUserById() throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.accounts().id(user.getId().get()).setActive(false);
+ setAssignee(r, user.getId().toString());
+ assertThat(getAssignee(r)._accountId).isEqualTo(user.getId().get());
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
index b5d3838..983ad21 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -306,7 +306,9 @@
gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@gerritcodereview.com>");
assertThat(result.error)
.isEqualTo(
- "Foo Bar <foo.bar@gerritcodereview.com> does not identify a registered user or group");
+ "Account 'Foo Bar <foo.bar@gerritcodereview.com>' not found\n"
+ + "Foo Bar <foo.bar@gerritcodereview.com> does not identify a registered user or"
+ + " group");
assertThat(result.reviewers).isNull();
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 5480ed1..9a6b823 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -48,6 +48,7 @@
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.submit.ChangeAlreadyMergedException;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.gerrit.testing.TestTimeUtil;
@@ -483,12 +484,20 @@
private ChangeInfo assertCreateSucceeds(ChangeInput in) throws Exception {
ChangeInfo out = gApi.changes().create(in).get();
assertThat(out.project).isEqualTo(in.project);
- assertThat(out.branch).isEqualTo(in.branch);
+ assertThat(RefNames.fullName(out.branch)).isEqualTo(RefNames.fullName(in.branch));
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);
- assertThat(out.workInProgress).isEqualTo(in.workInProgress);
+ if (in.isPrivate) {
+ assertThat(out.isPrivate).isTrue();
+ } else {
+ assertThat(out.isPrivate).isNull();
+ }
+ if (in.workInProgress) {
+ assertThat(out.workInProgress).isTrue();
+ } else {
+ assertThat(out.workInProgress).isNull();
+ }
assertThat(out.revisions).hasSize(1);
assertThat(out.submitted).isNull();
assertThat(in.status).isEqualTo(ChangeStatus.NEW);
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index af206f1..919b2fd 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -470,7 +470,7 @@
suggestReviewers(changeId, name), ImmutableList.of(foo1, foo2), ImmutableList.of());
gApi.accounts().id(foo2.username).setActive(false);
- assertThat(gApi.accounts().id(foo2.username).getActive()).isFalse();
+ assertThat(gApi.accounts().id(foo2.id.get()).getActive()).isFalse();
assertReviewers(suggestReviewers(changeId, name), ImmutableList.of(foo1), ImmutableList.of());
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
index a0bc450..7746820 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListChildProjectsIT.java
@@ -53,6 +53,17 @@
}
@Test
+ public void listChildrenWithLimit() throws Exception {
+ String prefix = RandomStringUtils.randomAlphabetic(8);
+ Project.NameKey child1 = projectOperations.newProject().name(prefix + "p1").create();
+ Project.NameKey child1_1 =
+ projectOperations.newProject().parent(child1).name(prefix + "p1.1").create();
+ projectOperations.newProject().parent(child1).name(prefix + "p1.2").create();
+
+ assertThatNameList(gApi.projects().name(child1.get()).children(1)).containsExactly(child1_1);
+ }
+
+ @Test
public void listChildrenRecursively() throws Exception {
String prefix = RandomStringUtils.randomAlphabetic(8);
Project.NameKey child1 = projectOperations.newProject().name(prefix + "p1").create();
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 0208926..a30e2b9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -17,7 +17,10 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertThatNameList;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static java.util.stream.Collectors.toList;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
@@ -33,12 +36,19 @@
import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.project.ProjectCacheImpl;
import com.google.gerrit.server.project.testing.Util;
+import com.google.gerrit.server.restapi.project.ListProjects;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
import com.google.inject.Inject;
+import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.stream.IntStream;
import org.junit.Test;
@NoHttpd
@@ -46,6 +56,7 @@
public class ListProjectsIT extends AbstractDaemonTest {
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ListProjects listProjects;
@Test
public void listProjects() throws Exception {
@@ -110,6 +121,63 @@
}
@Test
+ public void listProjectsToOutputStream() throws Exception {
+ int numInitialProjects = gApi.projects().list().get().size();
+ int numTestProjects = 5;
+ List<String> testProjects = createProjects("zzz_testProject", numTestProjects);
+ try (ByteArrayOutputStream displayOut = new ByteArrayOutputStream()) {
+
+ listProjects.setStart(numInitialProjects);
+ listProjects.displayToStream(displayOut);
+
+ List<String> lines =
+ Splitter.on("\n").omitEmptyStrings().splitToList(new String(displayOut.toByteArray()));
+ assertThat(lines).isEqualTo(testProjects);
+ }
+ }
+
+ @Test
+ public void listProjectsAsJsonMultilineToOutputStream() throws Exception {
+ listProjectsAsJsonToOutputStream(OutputFormat.JSON);
+ }
+
+ @Test
+ public void listProjectsAsJsonCompactToOutputStream() throws Exception {
+ String jsonOutput = listProjectsAsJsonToOutputStream(OutputFormat.JSON_COMPACT).trim();
+ assertThat(jsonOutput).doesNotContain("\n");
+ }
+
+ private String listProjectsAsJsonToOutputStream(OutputFormat jsonFormat) throws Exception {
+ assertThat(jsonFormat.isJson()).isTrue();
+
+ int numInitialProjects = gApi.projects().list().get().size();
+ int numTestProjects = 5;
+ Set<String> testProjects =
+ ImmutableSet.copyOf(createProjects("zzz_testProject", numTestProjects));
+ try (ByteArrayOutputStream displayOut = new ByteArrayOutputStream()) {
+
+ listProjects.setStart(numInitialProjects);
+ listProjects.setFormat(jsonFormat);
+ listProjects.displayToStream(displayOut);
+
+ String projectsJsonOutput = new String(displayOut.toByteArray());
+
+ Gson gson = jsonFormat.newGson();
+ Set<String> projectsJsonNames = gson.fromJson(projectsJsonOutput, JsonObject.class).keySet();
+ assertThat(projectsJsonNames).isEqualTo(testProjects);
+
+ return projectsJsonOutput;
+ }
+ }
+
+ private List<String> createProjects(String prefix, int numProjects) {
+ return IntStream.range(0, numProjects)
+ .mapToObj(i -> projectOperations.newProject().name(prefix + i).create())
+ .map(Project.NameKey::get)
+ .collect(toList());
+ }
+
+ @Test
public void listProjectsWithPrefix() throws Exception {
Project.NameKey someProject = projectOperations.newProject().name("listtest-p1").create();
Project.NameKey someOtherProject = projectOperations.newProject().name("listtest-p2").create();
diff --git a/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
new file mode 100644
index 0000000..d99fa72
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/account/AccountResolverIT.java
@@ -0,0 +1,357 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static com.google.common.truth.Truth8.assertThat;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.acceptance.testsuite.account.TestAccount;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.extensions.common.AccountVisibility;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.Result;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.notedb.Sequences;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class AccountResolverIT extends AbstractDaemonTest {
+ @ConfigSuite.Default
+ public static Config defaultConfig() {
+ Config cfg = new Config();
+ cfg.setEnum("accounts", null, "visibility", AccountVisibility.SAME_GROUP);
+ return cfg;
+ }
+
+ @Inject @ServerInitiated private Provider<AccountsUpdate> accountsUpdateProvider;
+ @Inject private AccountOperations accountOperations;
+ @Inject private AccountResolver accountResolver;
+ @Inject private Provider<CurrentUser> self;
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private Sequences sequences;
+
+ @Test
+ public void bySelf() throws Exception {
+ assertThat(resolve("Self")).isEmpty();
+ accountOperations.newAccount().fullname("self").create();
+
+ Result result = resolveAsResult("self");
+ assertThat(result.asIdSet()).containsExactly(admin.id);
+ assertThat(result.isSelf()).isTrue();
+ assertThat(result.asUniqueUser()).isSameAs(self.get());
+
+ result = resolveAsResult("me");
+ assertThat(result.asIdSet()).containsExactly(admin.id);
+ assertThat(result.isSelf()).isTrue();
+ assertThat(result.asUniqueUser()).isSameAs(self.get());
+
+ requestScopeOperations.setApiUserAnonymous();
+ checkBySelfFails();
+
+ requestScopeOperations.setApiUserInternal();
+ checkBySelfFails();
+ }
+
+ private void checkBySelfFails() throws Exception {
+ Result result = resolveAsResult("self");
+ assertThat(result.asIdSet()).isEmpty();
+ assertThat(result.isSelf()).isTrue();
+ try {
+ result.asUnique();
+ assert_().fail("expected UnresolvableAccountException");
+ } catch (UnresolvableAccountException e) {
+ assertThat(e).hasMessageThat().isEqualTo("Resolving account 'self' requires login");
+ assertThat(e.isSelf()).isTrue();
+ }
+
+ result = resolveAsResult("me");
+ assertThat(result.asIdSet()).isEmpty();
+ assertThat(result.isSelf()).isTrue();
+ try {
+ result.asUnique();
+ assert_().fail("expected UnresolvableAccountException");
+ } catch (UnresolvableAccountException e) {
+ assertThat(e).hasMessageThat().isEqualTo("Resolving account 'me' requires login");
+ assertThat(e.isSelf()).isTrue();
+ }
+ }
+
+ @Test
+ public void bySelfInactive() throws Exception {
+ gApi.accounts().id(user.id.get()).setActive(false);
+
+ requestScopeOperations.setApiUser(user.id);
+ assertThat(gApi.accounts().id("self").getActive()).isFalse();
+
+ Result result = resolveAsResult("self");
+ assertThat(result.asIdSet()).containsExactly(user.id);
+ assertThat(result.isSelf()).isTrue();
+ assertThat(result.asUniqueUser()).isSameAs(self.get());
+ }
+
+ @Test
+ public void byExactAccountId() throws Exception {
+ Account.Id existingId = accountOperations.newAccount().create();
+ Account.Id idWithExistingIdAsFullname =
+ accountOperations.newAccount().fullname(existingId.toString()).create();
+
+ Account.Id nonexistentId = new Account.Id(sequences.nextAccountId());
+ accountOperations.newAccount().fullname(nonexistentId.toString()).create();
+
+ assertThat(resolve(existingId)).containsExactly(existingId);
+ assertThat(resolve(nonexistentId)).isEmpty();
+
+ assertThat(resolveByNameOrEmail(existingId)).containsExactly(idWithExistingIdAsFullname);
+ }
+
+ @Test
+ public void byParenthesizedAccountId() throws Exception {
+ Account.Id existingId = accountOperations.newAccount().fullname("Test User").create();
+ accountOperations.newAccount().fullname(existingId.toString()).create();
+
+ Account.Id nonexistentId = new Account.Id(sequences.nextAccountId());
+ accountOperations.newAccount().fullname("Any Name (" + nonexistentId + ")").create();
+ accountOperations.newAccount().fullname(nonexistentId.toString()).create();
+
+ String existingInput = "Any Name (" + existingId + ")";
+ assertThat(resolve(existingInput)).containsExactly(existingId);
+ assertThat(resolve("Any Name (" + nonexistentId + ")")).isEmpty();
+
+ assertThat(resolveByNameOrEmail(existingInput)).isEmpty();
+ }
+
+ @Test
+ public void byUsername() throws Exception {
+ String existingUsername = "myusername";
+ Account.Id idWithUsername = accountOperations.newAccount().username(existingUsername).create();
+ Account.Id idWithExistingUsernameAsFullname =
+ accountOperations.newAccount().fullname(existingUsername).create();
+
+ String nonexistentUsername = "anotherusername";
+ Account.Id idWithFullname = accountOperations.newAccount().fullname("anotherusername").create();
+
+ assertThat(resolve(existingUsername)).containsExactly(idWithUsername);
+
+ // Doesn't short-circuit just because the input looks like a valid username.
+ assertThat(ExternalId.isValidUsername(nonexistentUsername)).isTrue();
+ assertThat(resolve(nonexistentUsername)).containsExactly(idWithFullname);
+
+ assertThat(resolveByNameOrEmail(existingUsername))
+ .containsExactly(idWithExistingUsernameAsFullname);
+ }
+
+ @Test
+ public void byNameAndEmail() throws Exception {
+ String email = name("user@example.com");
+ Account.Id idWithEmail = accountOperations.newAccount().preferredEmail(email).create();
+ accountOperations.newAccount().fullname(email).create();
+
+ String input = "First Last <" + email + ">";
+ assertThat(resolve(input)).containsExactly(idWithEmail);
+ assertThat(resolveByNameOrEmail(input)).containsExactly(idWithEmail);
+ }
+
+ @Test
+ public void byNameAndEmailPrefersAccountsWithMatchingFullName() throws Exception {
+ String email = name("user@example.com");
+ Account.Id id1 = accountOperations.newAccount().fullname("Aaa Bbb").create();
+ setPreferredEmailBypassingUniquenessCheck(id1, email);
+ Account.Id id2 = accountOperations.newAccount().fullname("Ccc Ddd").create();
+ setPreferredEmailBypassingUniquenessCheck(id2, email);
+
+ String input = "First Last <" + email + ">";
+ assertThat(resolve(input)).containsExactly(id1, id2);
+ assertThat(resolveByNameOrEmail(input)).containsExactly(id1, id2);
+
+ Account.Id id3 = accountOperations.newAccount().fullname("First Last").create();
+ setPreferredEmailBypassingUniquenessCheck(id3, email);
+ assertThat(resolve(input)).containsExactly(id3);
+ assertThat(resolveByNameOrEmail(input)).containsExactly(id3);
+
+ Account.Id id4 = accountOperations.newAccount().fullname("First Last").create();
+ setPreferredEmailBypassingUniquenessCheck(id4, email);
+ assertThat(resolve(input)).containsExactly(id3, id4);
+ assertThat(resolveByNameOrEmail(input)).containsExactly(id3, id4);
+ }
+
+ @Test
+ public void byEmail() throws Exception {
+ String email = name("user@example.com");
+ Account.Id idWithEmail = accountOperations.newAccount().preferredEmail(email).create();
+ accountOperations.newAccount().fullname(email).create();
+
+ assertThat(resolve(email)).containsExactly(idWithEmail);
+ assertThat(resolveByNameOrEmail(email)).containsExactly(idWithEmail);
+ }
+
+ // Can't test for ByRealm because DefaultRealm with the default (OPENID) auth type doesn't support
+ // email expansion, so anything that would return a non-null value from DefaultRealm#lookup would
+ // just be an email address, handled by other tests. This could be avoided if we inject some sort
+ // of custom test realm instance, but the ugliness is not worth it for this small bit of test
+ // coverage.
+
+ @Test
+ public void byFullName() throws Exception {
+ Account.Id id1 = accountOperations.newAccount().fullname("Somebodys Name").create();
+ accountOperations.newAccount().fullname("A totally different name").create();
+ String input = "Somebodys name";
+ assertThat(resolve(input)).containsExactly(id1);
+ assertThat(resolveByNameOrEmail(input)).containsExactly(id1);
+ }
+
+ @Test
+ public void byDefaultSearch() throws Exception {
+ Account.Id id1 = accountOperations.newAccount().fullname("John Doe").create();
+ Account.Id id2 = accountOperations.newAccount().fullname("Jane Doe").create();
+ assertThat(resolve("doe")).containsExactly(id1, id2);
+ assertThat(resolveByNameOrEmail("doe")).containsExactly(id1, id2);
+ }
+
+ @Test
+ public void onlyExactIdReturnsInactiveAccounts() throws Exception {
+ TestAccount account =
+ accountOperations
+ .account(
+ accountOperations
+ .newAccount()
+ .fullname("Inactiveuser Name")
+ .preferredEmail("inactiveuser@example.com")
+ .username("inactiveusername")
+ .create())
+ .get();
+ Account.Id id = account.accountId();
+ String nameEmail = account.fullname().get() + " <" + account.preferredEmail().get() + ">";
+ ImmutableList<String> inputs =
+ ImmutableList.of(
+ account.fullname().get() + " (" + account.accountId() + ")",
+ account.fullname().get(),
+ account.preferredEmail().get(),
+ nameEmail,
+ Splitter.on(' ').splitToList(account.fullname().get()).get(0));
+
+ assertThat(resolve(account.accountId())).containsExactly(id);
+ for (String input : inputs) {
+ assertThat(resolve(input)).named("results for %s (active)", input).containsExactly(id);
+ }
+
+ gApi.accounts().id(id.get()).setActive(false);
+ assertThat(resolve(account.accountId())).containsExactly(id);
+ for (String input : inputs) {
+ Result result = accountResolver.resolve(input);
+ assertThat(result.asIdSet()).named("results for %s (inactive)", input).isEmpty();
+ try {
+ result.asUnique();
+ assert_().fail("expected UnresolvableAccountException");
+ } catch (UnresolvableAccountException e) {
+ assertThat(e)
+ .hasMessageThat()
+ .isEqualTo(
+ "Account '"
+ + input
+ + "' only matches inactive accounts. To use an inactive account, retry"
+ + " with one of the following exact account IDs:\n"
+ + id
+ + ": "
+ + nameEmail);
+ }
+ assertThat(resolveByNameOrEmail(input))
+ .named("results by name or email for %s (inactive)", input)
+ .isEmpty();
+ }
+ }
+
+ @Test
+ public void filterVisibility() throws Exception {
+ Account.Id id1 =
+ accountOperations
+ .newAccount()
+ .fullname("John Doe")
+ .preferredEmail("johndoe@example.com")
+ .create();
+ Account.Id id2 =
+ accountOperations
+ .newAccount()
+ .fullname("Jane Doe")
+ .preferredEmail("janedoe@example.com")
+ .create();
+
+ // Admin can see all accounts. Use a variety of searches, including with/without
+ // callerMayAssumeCandidatesAreVisible.
+ assertThat(resolve(id1)).containsExactly(id1);
+ assertThat(resolve("John Doe")).containsExactly(id1);
+ assertThat(resolve("johndoe@example.com")).containsExactly(id1);
+ assertThat(resolve(id2)).containsExactly(id2);
+ assertThat(resolve("Jane Doe")).containsExactly(id2);
+ assertThat(resolve("janedoe@example.com")).containsExactly(id2);
+ assertThat(resolve("doe")).containsExactly(id1, id2);
+
+ // id2 can't see id1, and vice versa.
+ requestScopeOperations.setApiUser(id1);
+ assertThat(resolve(id1)).containsExactly(id1);
+ assertThat(resolve("John Doe")).containsExactly(id1);
+ assertThat(resolve("johndoe@example.com")).containsExactly(id1);
+ assertThat(resolve(id2)).isEmpty();
+ assertThat(resolve("Jane Doe")).isEmpty();
+ assertThat(resolve("janedoe@example.com")).isEmpty();
+ assertThat(resolve("doe")).containsExactly(id1);
+
+ requestScopeOperations.setApiUser(id2);
+ assertThat(resolve(id1)).isEmpty();
+ assertThat(resolve("John Doe")).isEmpty();
+ assertThat(resolve("johndoe@example.com")).isEmpty();
+ assertThat(resolve(id2)).containsExactly(id2);
+ assertThat(resolve("Jane Doe")).containsExactly(id2);
+ assertThat(resolve("janedoe@example.com")).containsExactly(id2);
+ assertThat(resolve("doe")).containsExactly(id2);
+ }
+
+ private ImmutableSet<Account.Id> resolve(Object input) throws Exception {
+ return resolveAsResult(input).asIdSet();
+ }
+
+ private Result resolveAsResult(Object input) throws Exception {
+ return accountResolver.resolve(input.toString());
+ }
+
+ @SuppressWarnings("deprecation")
+ private ImmutableSet<Account.Id> resolveByNameOrEmail(Object input) throws Exception {
+ return accountResolver.resolveByNameOrEmail(input.toString()).asIdSet();
+ }
+
+ private void setPreferredEmailBypassingUniquenessCheck(Account.Id id, String email)
+ throws Exception {
+ Optional<AccountState> result =
+ accountsUpdateProvider
+ .get()
+ .update("Force set preferred email", id, (s, u) -> u.setPreferredEmail(email));
+ assertThat(result.map(a -> a.getAccount().getPreferredEmail())).hasValue(email);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/account/BUILD b/javatests/com/google/gerrit/acceptance/server/account/BUILD
new file mode 100644
index 0000000..48fac99
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/account/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+ srcs = glob(["*IT.java"]),
+ group = "server_account",
+ labels = ["server"],
+)
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index 8bc78c3..2ee2d46 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -26,7 +26,6 @@
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.changes.FixInput;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ProblemInfo;
@@ -38,6 +37,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ConsistencyChecker;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -749,11 +749,11 @@
ChangeInserter ins;
try (BatchUpdate bu = newUpdate(owner.getId())) {
RevCommit commit = patchSetCommit(new PatchSet.Id(id, 1));
+ bu.setNotify(NotifyResolver.Result.none());
ins =
changeInserterFactory
.create(id, commit, dest)
.setValidate(false)
- .setNotify(NotifyHandling.NONE)
.setFireRevisionCreated(false)
.setSendMail(false);
bu.insertChange(ins).execute();
@@ -773,12 +773,12 @@
private ChangeNotes incrementPatchSet(ChangeNotes notes, RevCommit commit) throws Exception {
PatchSetInserter ins;
try (BatchUpdate bu = newUpdate(notes.getChange().getOwner())) {
+ bu.setNotify(NotifyResolver.Result.none());
ins =
patchSetInserterFactory
.create(notes, nextPatchSetId(notes), commit)
.setValidate(false)
- .setFireRevisionCreated(false)
- .setNotify(NotifyHandling.NONE);
+ .setFireRevisionCreated(false);
bu.addOp(notes.getChangeId(), ins).execute();
}
return reload(notes);
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
index 4b6e8c6..fc37f4c 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -99,6 +99,7 @@
.bcc(sc.starrer)
.bcc(ABANDONED_CHANGES)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -113,6 +114,7 @@
.bcc(sc.starrer)
.bcc(ABANDONED_CHANGES)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -128,6 +130,7 @@
.bcc(sc.starrer)
.bcc(ABANDONED_CHANGES)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -143,6 +146,7 @@
.bcc(sc.starrer)
.bcc(ABANDONED_CHANGES)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -154,13 +158,14 @@
.cc(sc.reviewer, sc.ccer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void abandonReviewableChangeNotifyOwner() throws Exception {
StagedChange sc = stageReviewableChange();
abandon(sc.changeId, sc.owner, OWNER);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -170,7 +175,7 @@
// Self-CC applies *after* need for sending notification is determined.
// Since there are no recipients before including the user taking action,
// there should no notification sent.
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -179,20 +184,21 @@
TestAccount other = accountCreator.create("other", "other@example.com", "other");
abandon(sc.changeId, other, CC_ON_OWN_COMMENTS, OWNER);
assertThat(sender).sent("abandon", sc).to(sc.owner).cc(other).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void abandonReviewableChangeNotifyNone() throws Exception {
StagedChange sc = stageReviewableChange();
abandon(sc.changeId, sc.owner, NONE);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
public void abandonReviewableChangeNotifyNoneCcingSelf() throws Exception {
StagedChange sc = stageReviewableChange();
abandon(sc.changeId, sc.owner, CC_ON_OWN_COMMENTS, NONE);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -206,13 +212,14 @@
.bcc(sc.starrer)
.bcc(ABANDONED_CHANGES)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void abandonWipChange() throws Exception {
StagedChange sc = stageWipChange();
abandon(sc.changeId, sc.owner);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -226,6 +233,7 @@
.bcc(sc.starrer)
.bcc(ABANDONED_CHANGES)
.noOneElse();
+ assertThat(sender).didNotSend();
}
private void abandon(String changeId, TestAccount by) throws Exception {
@@ -269,6 +277,7 @@
.cc(sc.reviewer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -292,6 +301,7 @@
.cc(sc.owner, sc.reviewer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -316,6 +326,7 @@
.cc(sc.owner, sc.reviewer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -340,6 +351,7 @@
.cc(sc.owner, sc.reviewer, other)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -363,6 +375,7 @@
.cc(sc.reviewer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -379,7 +392,7 @@
StagedChange sc = stageWipChange();
TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
addReviewer(adder, sc.changeId, sc.owner, reviewer.email);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -392,21 +405,32 @@
addReviewerToWipChange(batch());
}
- private void addReviewerToReviewableWipChange(Adder adder) throws Exception {
- StagedChange sc = stageReviewableWipChange();
- TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
- addReviewer(adder, sc.changeId, sc.owner, reviewer.email);
- assertThat(sender).notSent();
- }
-
@Test
public void addReviewerToReviewableWipChangeSingly() throws Exception {
- addReviewerToReviewableWipChange(singly());
+ StagedChange sc = stageReviewableWipChange();
+ TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
+ addReviewer(singly(), sc.changeId, sc.owner, reviewer.email);
+ // TODO(dborowitz): In theory this should match the batch case, but we don't currently pass
+ // enough info into AddReviewersEmail#emailReviewers to distinguish the reviewStarted case.
+ // Complicating the emailReviewers arguments is not the answer; this needs to be rewritten.
+ // Tolerate the difference for now.
+ assertThat(sender).didNotSend();
}
@Test
public void addReviewerToReviewableWipChangeBatch() throws Exception {
- addReviewerToReviewableWipChange(batch());
+ StagedChange sc = stageReviewableWipChange();
+ TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
+ addReviewer(batch(), sc.changeId, sc.owner, reviewer.email);
+ // For a review-started WIP change, same as in the notify=ALL case. It's not especially
+ // important to notify just because a reviewer is added, but we do want to notify in the other
+ // case that hits this codepath: posting an actual review.
+ assertThat(sender)
+ .sent("newchange", sc)
+ .to(reviewer)
+ .cc(sc.reviewer)
+ .cc(sc.reviewerByEmail, sc.ccerByEmail)
+ .noOneElse();
}
private void addReviewerToWipChangeNotifyAll(Adder adder) throws Exception {
@@ -420,6 +444,7 @@
.cc(sc.reviewer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -443,6 +468,7 @@
.cc(sc.reviewer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -460,7 +486,7 @@
StagedChange sc = stageReviewableChange();
TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
addReviewer(adder, sc.changeId, sc.owner, reviewer.email, CC_ON_OWN_COMMENTS, OWNER);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -478,7 +504,7 @@
StagedChange sc = stageReviewableChange();
TestAccount reviewer = accountCreator.create("added", "added@example.com", "added");
addReviewer(adder, sc.changeId, sc.owner, reviewer.email, CC_ON_OWN_COMMENTS, NONE);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -500,6 +526,7 @@
.cc(sc.reviewer)
.cc(sc.ccerByEmail, sc.reviewerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -521,6 +548,7 @@
.cc(sc.reviewer)
.cc(sc.ccerByEmail, sc.reviewerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -608,6 +636,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -622,6 +651,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -636,6 +666,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -650,6 +681,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -665,6 +697,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -680,6 +713,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -691,13 +725,14 @@
.cc(sc.reviewer, sc.ccer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void commentOnReviewableChangeByOwnerNotifyOwner() throws Exception {
StagedChange sc = stageReviewableChange();
review(sc.owner, sc.changeId, ENABLED, OWNER);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -705,14 +740,14 @@
StagedChange sc = stageReviewableChange();
setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
review(sc.owner, sc.changeId, ENABLED, OWNER);
- assertThat(sender).notSent(); // TODO(logan): Why not send to owner?
+ assertThat(sender).didNotSend(); // TODO(logan): Why not send to owner?
}
@Test
public void commentOnReviewableChangeByOwnerNotifyNone() throws Exception {
StagedChange sc = stageReviewableChange();
review(sc.owner, sc.changeId, ENABLED, NONE);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -720,7 +755,7 @@
StagedChange sc = stageReviewableChange();
setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
review(sc.owner, sc.changeId, ENABLED, NONE);
- assertThat(sender).notSent(); // TODO(logan): Why not send to owner?
+ assertThat(sender).didNotSend(); // TODO(logan): Why not send to owner?
}
@Test
@@ -734,20 +769,21 @@
.cc(sc.reviewer, sc.ccer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void commentOnWipChangeByOwner() throws Exception {
StagedChange sc = stageWipChange();
review(sc.owner, sc.changeId, ENABLED);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
public void commentOnWipChangeByOwnerCcingSelf() throws Exception {
StagedChange sc = stageWipChange();
review(sc.owner, sc.changeId, CC_ON_OWN_COMMENTS);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -761,6 +797,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -769,6 +806,7 @@
TestAccount bot = sc.testAccount("bot");
review(bot, sc.changeId, ENABLED, null, "autogenerated:tag");
assertThat(sender).sent("comment", sc).to(sc.owner).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -777,6 +815,7 @@
TestAccount bot = sc.testAccount("bot");
review(bot, sc.changeId, ENABLED, null, "autogenerated:tag");
assertThat(sender).sent("comment", sc).to(sc.owner).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -792,6 +831,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -805,6 +845,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -812,7 +853,7 @@
StagedChange sc = stageReviewableChange();
ReviewInput in = ReviewInput.noScore().setWorkInProgress(true);
gApi.changes().id(sc.changeId).revision("current").review(in);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -827,7 +868,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -842,7 +883,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -864,7 +905,7 @@
.cc(sc.reviewer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -918,12 +959,13 @@
.to(spc.watchingProjectOwner)
.bcc(NEW_CHANGES, NEW_PATCHSETS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void createWipChange() throws Exception {
stagePreChange("refs/for/master%wip");
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -931,7 +973,7 @@
setWorkInProgressByDefault(project, InheritableBoolean.TRUE);
StagedPreChange spc = stagePreChange("refs/for/master");
Truth.assertThat(gApi.changes().id(spc.changeId).get().workInProgress).isTrue();
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -939,7 +981,7 @@
// Make sure owner user is created
StagedChange sc = stageReviewableChange();
// All was cleaned already
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
// Toggle workInProgress flag for owner
GeneralPreferencesInfo prefs = gApi.accounts().id(sc.owner.id.get()).getPreferences();
@@ -949,7 +991,7 @@
// Create another change without notification that should be wip
StagedPreChange spc = stagePreChange("refs/for/master");
Truth.assertThat(gApi.changes().id(spc.changeId).get().workInProgress).isTrue();
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
// Clean up workInProgressByDefault by owner
prefs = gApi.accounts().id(sc.owner.id.get()).getPreferences();
@@ -961,19 +1003,19 @@
@Test
public void createReviewableChangeWithNotifyOwnerReviewers() throws Exception {
stagePreChange("refs/for/master%notify=OWNER_REVIEWERS");
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
public void createReviewableChangeWithNotifyOwner() throws Exception {
stagePreChange("refs/for/master%notify=OWNER");
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
public void createReviewableChangeWithNotifyNone() throws Exception {
stagePreChange("refs/for/master%notify=OWNER");
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -984,6 +1026,7 @@
.to(spc.watchingProjectOwner)
.bcc(NEW_CHANGES, NEW_PATCHSETS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -996,6 +1039,7 @@
assertThat(sender).sent("newchange", spc).to(spc.reviewer, spc.watchingProjectOwner);
subject.cc(spc.ccer);
subject.bcc(NEW_CHANGES, NEW_PATCHSETS).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1012,6 +1056,7 @@
.cc("nobody2@example.com")
.bcc(NEW_CHANGES, NEW_PATCHSETS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
/*
@@ -1031,6 +1076,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1046,6 +1092,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1061,6 +1108,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1077,6 +1125,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1092,6 +1141,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1105,13 +1155,14 @@
.cc(extraCcer, sc.reviewer, sc.ccer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void deleteReviewerFromReviewableChangeNotifyOwner() throws Exception {
StagedChange sc = stageReviewableChangeWithExtraReviewer();
removeReviewer(sc, extraReviewer, NotifyHandling.OWNER);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1120,13 +1171,14 @@
setEmailStrategy(sc.owner, EmailStrategy.CC_ON_OWN_COMMENTS);
removeReviewer(sc, extraReviewer, NotifyHandling.OWNER);
assertThat(sender).sent("deleteReviewer", sc).to(sc.owner, extraReviewer).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void deleteReviewerFromReviewableChangeNotifyNone() throws Exception {
StagedChange sc = stageReviewableChangeWithExtraReviewer();
removeReviewer(sc, extraReviewer, NotifyHandling.NONE);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1134,21 +1186,21 @@
StagedChange sc = stageReviewableChangeWithExtraReviewer();
setEmailStrategy(sc.owner, EmailStrategy.CC_ON_OWN_COMMENTS);
removeReviewer(sc, extraReviewer, NotifyHandling.NONE);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
public void deleteReviewerFromReviewableWipChange() throws Exception {
StagedChange sc = stageReviewableWipChangeWithExtraReviewer();
removeReviewer(sc, extraReviewer);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
public void deleteReviewerFromWipChange() throws Exception {
StagedChange sc = stageWipChangeWithExtraReviewer();
removeReviewer(sc, extraReviewer);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1164,6 +1216,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1173,6 +1226,7 @@
requestScopeOperations.setApiUser(sc.owner.getId());
removeReviewer(sc, extraReviewer);
assertThat(sender).sent("deleteReviewer", sc).to(extraReviewer).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1180,14 +1234,14 @@
StagedChange sc = stageWipChangeWithExtraReviewer();
recommend(sc, extraReviewer);
removeReviewer(sc, extraReviewer, NotifyHandling.OWNER);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
public void deleteReviewerByEmailFromWipChange() throws Exception {
StagedChange sc = stageWipChangeWithExtraReviewer();
gApi.changes().id(sc.changeId).reviewer(sc.reviewerByEmail).remove();
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
private void recommend(StagedChange sc, TestAccount by) throws Exception {
@@ -1207,11 +1261,14 @@
.reviewer(extraCcer.email, ReviewerState.CC, false);
requestScopeOperations.setApiUser(extraReviewer.getId());
gApi.changes().id(sc.changeId).revision("current").review(in);
+ sender.clear();
return sc;
}
private StagedChange stageReviewableChangeWithExtraReviewer() throws Exception {
- return stageChangeWithExtraReviewer(this::stageReviewableChange);
+ StagedChange sc = stageChangeWithExtraReviewer(this::stageReviewableChange);
+ sender.clear();
+ return sc;
}
private StagedChange stageReviewableWipChangeWithExtraReviewer() throws Exception {
@@ -1219,7 +1276,9 @@
}
private StagedChange stageWipChangeWithExtraReviewer() throws Exception {
- return stageChangeWithExtraReviewer(this::stageWipChange);
+ StagedChange sc = stageChangeWithExtraReviewer(this::stageWipChange);
+ assertThat(sender).didNotSend();
+ return sc;
}
private void removeReviewer(StagedChange sc, TestAccount account) throws Exception {
@@ -1252,6 +1311,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1269,6 +1329,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1285,6 +1346,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1302,6 +1364,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1315,6 +1378,7 @@
.cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1330,6 +1394,7 @@
.cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1339,6 +1404,7 @@
requestScopeOperations.setApiUser(admin.getId());
deleteVote(sc, extraReviewer, NotifyHandling.OWNER);
assertThat(sender).sent("deleteVote", sc).to(sc.owner).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1347,7 +1413,7 @@
recommend(sc, extraReviewer);
requestScopeOperations.setApiUser(sc.owner.getId());
deleteVote(sc, extraReviewer, NotifyHandling.NONE);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1357,7 +1423,7 @@
setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS);
requestScopeOperations.setApiUser(sc.owner.getId());
deleteVote(sc, extraReviewer, NotifyHandling.NONE);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1373,6 +1439,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1388,6 +1455,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
private void deleteVote(StagedChange sc, TestAccount account) throws Exception {
@@ -1419,6 +1487,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1433,6 +1502,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1447,6 +1517,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1461,6 +1532,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS, SUBMITTED_CHANGES)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1473,6 +1545,7 @@
.cc(sc.reviewer, sc.ccer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1480,6 +1553,7 @@
StagedChange sc = stageChangeReadyForMerge();
merge(sc.changeId, other, OWNER);
assertThat(sender).sent("merged", sc).to(sc.owner).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1488,13 +1562,14 @@
setEmailStrategy(other, EmailStrategy.CC_ON_OWN_COMMENTS);
merge(sc.changeId, other, OWNER);
assertThat(sender).sent("merged", sc).to(sc.owner).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void mergeByOtherNotifyNone() throws Exception {
StagedChange sc = stageChangeReadyForMerge();
merge(sc.changeId, other, NONE);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1502,7 +1577,7 @@
StagedChange sc = stageChangeReadyForMerge();
setEmailStrategy(other, EmailStrategy.CC_ON_OWN_COMMENTS);
merge(sc.changeId, other, NONE);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
private void merge(String changeId, TestAccount by) throws Exception {
@@ -1554,6 +1629,7 @@
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1569,6 +1645,7 @@
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1584,6 +1661,7 @@
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1598,6 +1676,7 @@
.cc(sc.ccer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1613,13 +1692,14 @@
.cc(sc.ccer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void newPatchSetByOtherOnReviewableChangeNotifyOwner() throws Exception {
StagedChange sc = stageReviewableChange();
pushTo(sc, "refs/for/master%notify=OWNER", other);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1628,7 +1708,7 @@
pushTo(sc, "refs/for/master%notify=OWNER", other, EmailStrategy.CC_ON_OWN_COMMENTS);
// TODO(logan): This email shouldn't come from the owner, and that's why
// no email is currently sent (owner isn't CCing self).
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1637,28 +1717,28 @@
pushTo(sc, "refs/for/master%notify=NONE", other);
// TODO(logan): This email shouldn't come from the owner, and that's why
// no email is currently sent (owner isn't CCing self).
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcNotifyNone() throws Exception {
StagedChange sc = stageReviewableChange();
pushTo(sc, "refs/for/master%notify=NONE", other, EmailStrategy.CC_ON_OWN_COMMENTS);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
public void newPatchSetByOwnerOnReviewableChangeToWip() throws Exception {
StagedChange sc = stageReviewableChange();
pushTo(sc, "refs/for/master%wip", sc.owner);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
public void newPatchSetOnWipChange() throws Exception {
StagedChange sc = stageWipChange();
pushTo(sc, "refs/for/master%wip", sc.owner);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1673,6 +1753,7 @@
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1687,13 +1768,14 @@
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void newPatchSetOnReviewableWipChange() throws Exception {
StagedChange sc = stageReviewableWipChange();
pushTo(sc, "refs/for/master%wip", sc.owner);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1709,7 +1791,7 @@
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1717,7 +1799,7 @@
StagedChange sc = stageWipChange();
TestAccount newReviewer = sc.testAccount("newReviewer");
pushTo(sc, "refs/for/master%r=" + newReviewer.username, sc.owner);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1733,7 +1815,7 @@
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1748,7 +1830,7 @@
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
private void pushTo(StagedChange sc, String ref, TestAccount by) throws Exception {
@@ -1773,6 +1855,7 @@
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1787,6 +1870,7 @@
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1801,6 +1885,7 @@
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1813,6 +1898,7 @@
.cc(sc.ccer)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1826,6 +1912,7 @@
.cc(sc.ccer, other)
.cc(sc.reviewerByEmail, sc.ccerByEmail)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1833,6 +1920,7 @@
StagedChange sc = stageReviewableChange();
editCommitMessage(sc, other, OWNER);
assertThat(sender).sent("newpatchset", sc).to(sc.owner).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1840,27 +1928,28 @@
StagedChange sc = stageReviewableChange();
editCommitMessage(sc, other, OWNER, CC_ON_OWN_COMMENTS);
assertThat(sender).sent("newpatchset", sc).to(sc.owner).cc(other).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void editCommitMessageByOtherOnReviewableChangeNotifyNone() throws Exception {
StagedChange sc = stageReviewableChange();
editCommitMessage(sc, other, NONE);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
public void editCommitMessageByOtherOnReviewableChangeOwnerSelfCcNotifyNone() throws Exception {
StagedChange sc = stageReviewableChange();
editCommitMessage(sc, other, NONE, CC_ON_OWN_COMMENTS);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
public void editCommitMessageOnWipChange() throws Exception {
StagedChange sc = stageWipChange();
editCommitMessage(sc, sc.owner);
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1868,6 +1957,7 @@
StagedChange sc = stageWipChange();
editCommitMessage(sc, other);
assertThat(sender).sent("newpatchset", sc).to(sc.owner).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1875,6 +1965,7 @@
StagedChange sc = stageWipChange();
editCommitMessage(sc, other, CC_ON_OWN_COMMENTS);
assertThat(sender).sent("newpatchset", sc).to(sc.owner).cc(other).noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1889,6 +1980,7 @@
.bcc(sc.starrer)
.bcc(NEW_PATCHSETS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
private void editCommitMessage(StagedChange sc, TestAccount by) throws Exception {
@@ -1931,6 +2023,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1944,6 +2037,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1957,6 +2051,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1971,6 +2066,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1985,6 +2081,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -1999,6 +2096,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
private void restore(String changeId, TestAccount by) throws Exception {
@@ -2037,6 +2135,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -2061,6 +2160,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -2085,6 +2185,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -2109,6 +2210,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
private StagedChange stageChange() throws Exception {
@@ -2144,6 +2246,7 @@
.cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
.to(sc.assignee)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -2156,6 +2259,7 @@
.cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
.to(sc.assignee)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -2167,6 +2271,7 @@
.cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
.to(sc.assignee)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -2179,6 +2284,7 @@
.cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
.to(sc.assignee)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -2189,6 +2295,7 @@
.sent("setassignee", sc)
.cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -2203,6 +2310,7 @@
.cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
.to(sc.assignee)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -2215,6 +2323,7 @@
.sent("setassignee", sc)
.cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -2226,6 +2335,7 @@
.cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
.to(sc.assignee)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -2237,6 +2347,7 @@
.cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended!
.to(sc.assignee)
.noOneElse();
+ assertThat(sender).didNotSend();
}
private void assign(StagedChange sc, TestAccount by, TestAccount to) throws Exception {
@@ -2267,6 +2378,7 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
@@ -2282,20 +2394,19 @@
.bcc(sc.starrer)
.bcc(ALL_COMMENTS)
.noOneElse();
+ assertThat(sender).didNotSend();
}
@Test
public void setWorkInProgress() throws Exception {
StagedChange sc = stageReviewableChange();
gApi.changes().id(sc.changeId).setWorkInProgress();
- assertThat(sender).notSent();
+ assertThat(sender).didNotSend();
}
private void startReview(StagedChange sc) throws Exception {
requestScopeOperations.setApiUser(sc.owner.getId());
gApi.changes().id(sc.changeId).setReadyForReview();
- // PolyGerrit current immediately follows up with a review.
- gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.noScore());
}
private void setWorkInProgressByDefault(Project.NameKey p, InheritableBoolean v)
diff --git a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
index 4e88955..05516d5 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/ElasticIndexIT.java
@@ -31,7 +31,7 @@
@ConfigSuite.Config
public static Config elasticsearchV6() {
- return getConfig(ElasticVersion.V6_5);
+ return getConfig(ElasticVersion.V6_6);
}
@ConfigSuite.Config
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
index 954b0e6..64dcfc2 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
@@ -31,14 +31,10 @@
import java.sql.Timestamp;
import java.util.Objects;
import java.util.Optional;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
public class GroupOperationsImplTest extends AbstractDaemonTest {
- @Rule public ExpectedException expectedException = ExpectedException.none();
-
@Inject private AccountOperations accountOperations;
@Inject private GroupOperationsImpl groupOperations;
@@ -231,7 +227,7 @@
public void retrievingNotExistingGroupFails() throws Exception {
AccountGroup.UUID notExistingGroupUuid = new AccountGroup.UUID("not-existing-group");
- expectedException.expect(IllegalStateException.class);
+ exception.expect(IllegalStateException.class);
groupOperations.group(notExistingGroupUuid).get();
}
diff --git a/javatests/com/google/gerrit/elasticsearch/BUILD b/javatests/com/google/gerrit/elasticsearch/BUILD
index f9c867b..62c3fe1 100644
--- a/javatests/com/google/gerrit/elasticsearch/BUILD
+++ b/javatests/com/google/gerrit/elasticsearch/BUILD
@@ -93,6 +93,7 @@
"//java/com/google/gerrit/testing:gerrit-test-util",
"//lib:guava",
"//lib/guice",
+ "//lib/httpcomponents:httpcore",
"//lib/jgit/org.eclipse.jgit:jgit",
"//lib/truth",
],
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
index 735354e..2f630ad 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticConfigurationTest.java
@@ -31,6 +31,7 @@
import com.google.inject.ProvisionException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
+import org.apache.http.HttpHost;
import org.eclipse.jgit.lib.Config;
import org.junit.Test;
@@ -138,7 +139,7 @@
}
private void assertHosts(ElasticConfiguration cfg, Object... hostURIs) throws Exception {
- assertThat(Arrays.asList(cfg.getHosts()).stream().map(h -> h.toURI()).collect(toList()))
+ assertThat(Arrays.asList(cfg.getHosts()).stream().map(HttpHost::toURI).collect(toList()))
.containsExactly(hostURIs);
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 8583d2d..e0f1e3a 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -46,6 +46,8 @@
return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.4.3";
case V6_5:
return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.5.4";
+ case V6_6:
+ return "docker.elastic.co/elasticsearch/elasticsearch-oss:6.6.0";
case V7_0:
return "docker.elastic.co/elasticsearch/elasticsearch-oss:7.0.0-alpha2";
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
index 53593ef..b585960 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryAccountsTest.java
@@ -41,7 +41,7 @@
return;
}
- container = ElasticContainer.createAndStart(ElasticVersion.V6_5);
+ container = ElasticContainer.createAndStart(ElasticVersion.V6_6);
nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
index 6429431..7854acd 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryChangesTest.java
@@ -41,7 +41,7 @@
return;
}
- container = ElasticContainer.createAndStart(ElasticVersion.V6_5);
+ container = ElasticContainer.createAndStart(ElasticVersion.V6_6);
nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
index de0af97..25932ce 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticV6QueryGroupsTest.java
@@ -41,7 +41,7 @@
return;
}
- container = ElasticContainer.createAndStart(ElasticVersion.V6_5);
+ container = ElasticContainer.createAndStart(ElasticVersion.V6_6);
nodeInfo = new ElasticNodeInfo(container.getHttpHost().getPort());
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index baf6c2b..e6ca7cf 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -37,6 +37,9 @@
assertThat(ElasticVersion.forVersion("6.5.0")).isEqualTo(ElasticVersion.V6_5);
assertThat(ElasticVersion.forVersion("6.5.1")).isEqualTo(ElasticVersion.V6_5);
+ assertThat(ElasticVersion.forVersion("6.6.0")).isEqualTo(ElasticVersion.V6_6);
+ assertThat(ElasticVersion.forVersion("6.6.1")).isEqualTo(ElasticVersion.V6_6);
+
assertThat(ElasticVersion.forVersion("7.0.0")).isEqualTo(ElasticVersion.V7_0);
assertThat(ElasticVersion.forVersion("7.0.1")).isEqualTo(ElasticVersion.V7_0);
}
@@ -55,6 +58,8 @@
assertThat(ElasticVersion.V6_2.isV6OrLater()).isTrue();
assertThat(ElasticVersion.V6_3.isV6OrLater()).isTrue();
assertThat(ElasticVersion.V6_4.isV6OrLater()).isTrue();
+ assertThat(ElasticVersion.V6_5.isV6OrLater()).isTrue();
+ assertThat(ElasticVersion.V6_6.isV6OrLater()).isTrue();
assertThat(ElasticVersion.V7_0.isV6OrLater()).isTrue();
}
@@ -64,6 +69,8 @@
assertThat(ElasticVersion.V6_2.isV7OrLater()).isFalse();
assertThat(ElasticVersion.V6_3.isV7OrLater()).isFalse();
assertThat(ElasticVersion.V6_4.isV7OrLater()).isFalse();
+ assertThat(ElasticVersion.V6_5.isV7OrLater()).isFalse();
+ assertThat(ElasticVersion.V6_6.isV7OrLater()).isFalse();
assertThat(ElasticVersion.V7_0.isV7OrLater()).isTrue();
}
}
diff --git a/javatests/com/google/gerrit/server/account/AccountResolverTest.java b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
new file mode 100644
index 0000000..fefbc2f
--- /dev/null
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -0,0 +1,367 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.Iterables.getOnlyElement;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
+import static java.util.stream.Collectors.joining;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.AccountResolver.Result;
+import com.google.gerrit.server.account.AccountResolver.Searcher;
+import com.google.gerrit.server.account.AccountResolver.StringSearcher;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.util.time.TimeUtil;
+import com.google.gerrit.testing.GerritBaseTests;
+import java.util.Arrays;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+import org.junit.Test;
+
+public class AccountResolverTest extends GerritBaseTests {
+ private class TestSearcher extends StringSearcher {
+ private final String pattern;
+ private final boolean shortCircuit;
+ private final ImmutableList<AccountState> accounts;
+ private boolean assumeVisible;
+ private boolean filterInactive;
+
+ private TestSearcher(String pattern, boolean shortCircuit, AccountState... accounts) {
+ this.pattern = pattern;
+ this.shortCircuit = shortCircuit;
+ this.accounts = ImmutableList.copyOf(accounts);
+ }
+
+ @Override
+ protected boolean matches(String input) {
+ return input.matches(pattern);
+ }
+
+ @Override
+ public Stream<AccountState> search(String input) {
+ return accounts.stream();
+ }
+
+ @Override
+ public boolean shortCircuitIfNoResults() {
+ return shortCircuit;
+ }
+
+ @Override
+ public boolean callerMayAssumeCandidatesAreVisible() {
+ return assumeVisible;
+ }
+
+ void setCallerMayAssumeCandidatesAreVisible() {
+ this.assumeVisible = true;
+ }
+
+ @Override
+ public boolean callerShouldFilterOutInactiveCandidates() {
+ return filterInactive;
+ }
+
+ void setCallerShouldFilterOutInactiveCandidates() {
+ this.filterInactive = true;
+ }
+
+ @Override
+ public String toString() {
+ return accounts
+ .stream()
+ .map(a -> a.getAccount().getId().toString())
+ .collect(joining(",", pattern + "(", ")"));
+ }
+ }
+
+ @Test
+ public void noShortCircuit() throws Exception {
+ ImmutableList<Searcher<?>> searchers =
+ ImmutableList.of(
+ new TestSearcher("foo", false, newAccount(1)),
+ new TestSearcher("bar", false, newAccount(2), newAccount(3)));
+
+ Result result = search("foo", searchers, allVisible());
+ assertThat(result.input()).isEqualTo("foo");
+ assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1));
+
+ result = search("bar", searchers, allVisible());
+ assertThat(result.input()).isEqualTo("bar");
+ assertThat(result.asIdSet()).containsExactlyElementsIn(ids(2, 3));
+
+ result = search("baz", searchers, allVisible());
+ assertThat(result.input()).isEqualTo("baz");
+ assertThat(result.asIdSet()).isEmpty();
+ }
+
+ @Test
+ public void shortCircuit() throws Exception {
+ ImmutableList<Searcher<?>> searchers =
+ ImmutableList.of(
+ new TestSearcher("f.*", true), new TestSearcher("foo|bar", false, newAccount(1)));
+
+ Result result = search("foo", searchers, allVisible());
+ assertThat(result.input()).isEqualTo("foo");
+ assertThat(result.asIdSet()).isEmpty();
+
+ result = search("bar", searchers, allVisible());
+ assertThat(result.input()).isEqualTo("bar");
+ assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1));
+ }
+
+ @Test
+ public void filterInvisible() throws Exception {
+ ImmutableList<Searcher<?>> searchers =
+ ImmutableList.of(new TestSearcher("foo", false, newAccount(1), newAccount(2)));
+
+ assertThat(search("foo", searchers, allVisible()).asIdSet())
+ .containsExactlyElementsIn(ids(1, 2));
+ assertThat(search("foo", searchers, only(2)).asIdSet()).containsExactlyElementsIn(ids(2));
+ }
+
+ @Test
+ public void skipVisibilityCheck() throws Exception {
+ TestSearcher searcher = new TestSearcher("foo", false, newAccount(1), newAccount(2));
+ ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher);
+
+ assertThat(search("foo", searchers, only(2)).asIdSet()).containsExactlyElementsIn(ids(2));
+
+ searcher.setCallerMayAssumeCandidatesAreVisible();
+ assertThat(search("foo", searchers, only(2)).asIdSet()).containsExactlyElementsIn(ids(1, 2));
+ }
+
+ @Test
+ public void dontFilterInactive() throws Exception {
+ ImmutableList<Searcher<?>> searchers =
+ ImmutableList.of(
+ new TestSearcher("foo", false, newInactiveAccount(1)),
+ new TestSearcher("f.*", false, newInactiveAccount(2)));
+
+ Result result = search("foo", searchers, allVisible());
+ // Searchers always short-circuit when finding a non-empty result list, and this one didn't
+ // filter out inactive results, so the second searcher never ran.
+ assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1));
+ assertThat(getOnlyElement(result.asList()).getAccount().isActive()).isFalse();
+ assertThat(filteredInactiveIds(result)).isEmpty();
+ }
+
+ @Test
+ public void filterInactiveEventuallyFindingResults() throws Exception {
+ TestSearcher searcher1 = new TestSearcher("foo", false, newInactiveAccount(1));
+ searcher1.setCallerShouldFilterOutInactiveCandidates();
+ TestSearcher searcher2 = new TestSearcher("f.*", false, newAccount(2));
+ searcher2.setCallerShouldFilterOutInactiveCandidates();
+ ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2);
+
+ Result result = search("foo", searchers, allVisible());
+ assertThat(search("foo", searchers, allVisible()).asIdSet()).containsExactlyElementsIn(ids(2));
+ // No info about inactive results exposed if there was at least one active result.
+ assertThat(filteredInactiveIds(result)).isEmpty();
+ }
+
+ @Test
+ public void filterInactiveEventuallyFindingNoResults() throws Exception {
+ TestSearcher searcher1 = new TestSearcher("foo", false, newInactiveAccount(1));
+ searcher1.setCallerShouldFilterOutInactiveCandidates();
+ TestSearcher searcher2 = new TestSearcher("f.*", false, newInactiveAccount(2));
+ searcher2.setCallerShouldFilterOutInactiveCandidates();
+ ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2);
+
+ Result result = search("foo", searchers, allVisible());
+ assertThat(result.asIdSet()).isEmpty();
+ assertThat(filteredInactiveIds(result)).containsExactlyElementsIn(ids(1, 2));
+ }
+
+ @Test
+ public void dontShortCircuitAfterFilteringInactiveCandidatesResultsInEmptyList()
+ throws Exception {
+ AccountState account1 = newAccount(1);
+ AccountState account2 = newInactiveAccount(2);
+ TestSearcher searcher1 = new TestSearcher("foo", false, account2);
+ searcher1.setCallerShouldFilterOutInactiveCandidates();
+
+ TestSearcher searcher2 = new TestSearcher("foo", false, account1, account2);
+ ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2);
+
+ // searcher1 matched, but filtered out all candidates because account2 is inactive. Actual
+ // result came from searcher2 instead.
+ Result result = search("foo", searchers, allVisible());
+ assertThat(result.asIdSet()).containsExactlyElementsIn(ids(1, 2));
+ }
+
+ @Test
+ public void shortCircuitAfterFilteringInactiveCandidatesResultsInEmptyList() throws Exception {
+ AccountState account1 = newAccount(1);
+ AccountState account2 = newInactiveAccount(2);
+ TestSearcher searcher1 = new TestSearcher("foo", true, account2);
+ searcher1.setCallerShouldFilterOutInactiveCandidates();
+
+ TestSearcher searcher2 = new TestSearcher("foo", false, account1, account2);
+ ImmutableList<Searcher<?>> searchers = ImmutableList.of(searcher1, searcher2);
+
+ // searcher1 matched and then filtered out all candidates because account2 is inactive, but
+ // still short-circuited.
+ Result result = search("foo", searchers, allVisible());
+ assertThat(result.asIdSet()).isEmpty();
+ assertThat(filteredInactiveIds(result)).containsExactlyElementsIn(ids(2));
+ }
+
+ @Test
+ public void asUniqueWithNoResults() throws Exception {
+ try {
+ String input = "foo";
+ ImmutableList<Searcher<?>> searchers = ImmutableList.of();
+ Supplier<Predicate<AccountState>> visibilitySupplier = allVisible();
+ search(input, searchers, visibilitySupplier).asUnique();
+ assert_().fail("Expected UnresolvableAccountException");
+ } catch (UnresolvableAccountException e) {
+ assertThat(e).hasMessageThat().isEqualTo("Account 'foo' not found");
+ }
+ }
+
+ @Test
+ public void asUniqueWithOneResult() throws Exception {
+ AccountState account = newAccount(1);
+ ImmutableList<Searcher<?>> searchers =
+ ImmutableList.of(new TestSearcher("foo", false, account));
+ assertThat(search("foo", searchers, allVisible()).asUnique().getAccount().getId())
+ .isEqualTo(account.getAccount().getId());
+ }
+
+ @Test
+ public void asUniqueWithMultipleResults() throws Exception {
+ ImmutableList<Searcher<?>> searchers =
+ ImmutableList.of(new TestSearcher("foo", false, newAccount(1), newAccount(2)));
+ try {
+ search("foo", searchers, allVisible()).asUnique();
+ assert_().fail("Expected UnresolvableAccountException");
+ } catch (UnresolvableAccountException e) {
+ assertThat(e)
+ .hasMessageThat()
+ .isEqualTo("Account 'foo' is ambiguous:\n1: Anonymous Name (1)\n2: Anonymous Name (2)");
+ }
+ }
+
+ @Test
+ public void exceptionMessageNotFound() throws Exception {
+ AccountResolver resolver = newAccountResolver();
+ assertThat(
+ new UnresolvableAccountException(
+ resolver.new Result("foo", ImmutableList.of(), ImmutableList.of())))
+ .hasMessageThat()
+ .isEqualTo("Account 'foo' not found");
+ }
+
+ @Test
+ public void exceptionMessageSelf() throws Exception {
+ AccountResolver resolver = newAccountResolver();
+ UnresolvableAccountException e =
+ new UnresolvableAccountException(
+ resolver.new Result("self", ImmutableList.of(), ImmutableList.of()));
+ assertThat(e.isSelf()).isTrue();
+ assertThat(e).hasMessageThat().isEqualTo("Resolving account 'self' requires login");
+ }
+
+ @Test
+ public void exceptionMessageMe() throws Exception {
+ AccountResolver resolver = newAccountResolver();
+ UnresolvableAccountException e =
+ new UnresolvableAccountException(
+ resolver.new Result("me", ImmutableList.of(), ImmutableList.of()));
+ assertThat(e.isSelf()).isTrue();
+ assertThat(e).hasMessageThat().isEqualTo("Resolving account 'me' requires login");
+ }
+
+ @Test
+ public void exceptionMessageAmbiguous() throws Exception {
+ AccountResolver resolver = newAccountResolver();
+ assertThat(
+ new UnresolvableAccountException(
+ resolver
+ .new Result(
+ "foo", ImmutableList.of(newAccount(3), newAccount(1)), ImmutableList.of())))
+ .hasMessageThat()
+ .isEqualTo("Account 'foo' is ambiguous:\n1: Anonymous Name (1)\n3: Anonymous Name (3)");
+ }
+
+ @Test
+ public void exceptionMessageOnlyInactive() throws Exception {
+ AccountResolver resolver = newAccountResolver();
+ assertThat(
+ new UnresolvableAccountException(
+ resolver
+ .new Result(
+ "foo",
+ ImmutableList.of(),
+ ImmutableList.of(newInactiveAccount(3), newInactiveAccount(1)))))
+ .hasMessageThat()
+ .isEqualTo(
+ "Account 'foo' only matches inactive accounts. To use an inactive account, retry"
+ + " with one of the following exact account IDs:\n"
+ + "1: Anonymous Name (1)\n"
+ + "3: Anonymous Name (3)");
+ }
+
+ private Result search(
+ String input,
+ ImmutableList<Searcher<?>> searchers,
+ Supplier<Predicate<AccountState>> visibilitySupplier)
+ throws Exception {
+ return newAccountResolver().searchImpl(input, searchers, visibilitySupplier);
+ }
+
+ private static AccountResolver newAccountResolver() {
+ return new AccountResolver(null, null, null, null, null, null, null, "Anonymous Name");
+ }
+
+ private AccountState newAccount(int id) {
+ return AccountState.forAccount(
+ new AllUsersName("All-Users"), new Account(new Account.Id(id), TimeUtil.nowTs()));
+ }
+
+ private AccountState newInactiveAccount(int id) {
+ Account a = new Account(new Account.Id(id), TimeUtil.nowTs());
+ a.setActive(false);
+ return AccountState.forAccount(new AllUsersName("All-Users"), a);
+ }
+
+ private static ImmutableSet<Account.Id> ids(int... ids) {
+ return Arrays.stream(ids).mapToObj(Account.Id::new).collect(toImmutableSet());
+ }
+
+ private static Supplier<Predicate<AccountState>> allVisible() {
+ return () -> a -> true;
+ }
+
+ private static Supplier<Predicate<AccountState>> only(int... ids) {
+ ImmutableSet<Account.Id> idSet =
+ Arrays.stream(ids).mapToObj(Account.Id::new).collect(toImmutableSet());
+ return () -> a -> idSet.contains(a.getAccount().getId());
+ }
+
+ private static ImmutableSet<Account.Id> filteredInactiveIds(Result result) {
+ return result
+ .filteredInactive()
+ .stream()
+ .map(a -> a.getAccount().getId())
+ .collect(toImmutableSet());
+ }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 79eae2a..5eecd0f 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -49,7 +49,6 @@
import com.google.gerrit.extensions.api.changes.Changes.QueryRequest;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.HashtagsInput;
-import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.StarsInput;
@@ -64,6 +63,7 @@
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.index.FieldDef;
@@ -77,6 +77,7 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
@@ -90,6 +91,7 @@
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.change.ChangeTriplet;
+import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -155,6 +157,7 @@
@Inject protected ChangeIndexer indexer;
@Inject protected IndexConfig indexConfig;
@Inject protected InMemoryRepositoryManager repoManager;
+ @Inject protected Provider<AnonymousUser> anonymousUserProvider;
@Inject protected Provider<InternalChangeQuery> queryProvider;
@Inject protected ChangeNotes.Factory notesFactory;
@Inject protected OneOffRequestContext oneOffRequestContext;
@@ -1379,7 +1382,7 @@
Change change1 = insert(repo, newChangeWithFiles(repo, "foo.h", "foo.cc"));
Change change2 = insert(repo, newChangeWithFiles(repo, "bar.H", "bar.CC"));
Change change3 = insert(repo, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
- Change change4 = insert(repo, newChangeWithFiles(repo, "Quux.java"));
+ Change change4 = insert(repo, newChangeWithFiles(repo, "Quux.java", "foo"));
assertQuery("extension:java", change4);
assertQuery("ext:java", change4);
@@ -1387,6 +1390,215 @@
assertQuery("ext:jAvA", change4);
assertQuery("ext:.jAvA", change4);
assertQuery("ext:cc", change3, change2, change1);
+
+ if (getSchemaVersion() >= 56) {
+ // matching changes with files that have no extension is possible
+ assertQuery("ext:\"\"", change4);
+ assertFailingQuery("ext:");
+ }
+ }
+
+ @Test
+ public void byOnlyExtensions() throws Exception {
+ if (getSchemaVersion() < 53) {
+ assertMissingField(ChangeField.ONLY_EXTENSIONS);
+ String unsupportedOperatorMessage =
+ "'onlyextensions' operator is not supported by change index version";
+ assertFailingQuery("onlyextensions:txt,jpg", unsupportedOperatorMessage);
+ assertFailingQuery("onlyexts:txt,jpg", unsupportedOperatorMessage);
+ return;
+ }
+
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(repo, newChangeWithFiles(repo, "foo.h", "foo.cc", "bar.cc"));
+ Change change2 = insert(repo, newChangeWithFiles(repo, "bar.H", "bar.CC", "foo.H"));
+ Change change3 = insert(repo, newChangeWithFiles(repo, "foo.CC", "bar.cc"));
+ Change change4 = insert(repo, newChangeWithFiles(repo, "dir/baz.h", "dir/baz.cc"));
+ Change change5 = insert(repo, newChangeWithFiles(repo, "Quux.java"));
+ Change change6 = insert(repo, newChangeWithFiles(repo, "foo.txt", "foo"));
+ Change change7 = insert(repo, newChangeWithFiles(repo, "foo"));
+
+ // case doesn't matter
+ assertQuery("onlyextensions:cc,h", change4, change2, change1);
+ assertQuery("onlyextensions:CC,H", change4, change2, change1);
+ assertQuery("onlyextensions:cc,H", change4, change2, change1);
+ assertQuery("onlyextensions:cC,h", change4, change2, change1);
+ assertQuery("onlyextensions:cc", change3);
+ assertQuery("onlyextensions:CC", change3);
+ assertQuery("onlyexts:java", change5);
+ assertQuery("onlyexts:jAvA", change5);
+ assertQuery("onlyexts:.jAvA", change5);
+
+ // order doesn't matter
+ assertQuery("onlyextensions:h,cc", change4, change2, change1);
+ assertQuery("onlyextensions:H,CC", change4, change2, change1);
+
+ // specifying extension with '.' is okay
+ assertQuery("onlyextensions:.cc,.h", change4, change2, change1);
+ assertQuery("onlyextensions:cc,.h", change4, change2, change1);
+ assertQuery("onlyextensions:.cc,h", change4, change2, change1);
+ assertQuery("onlyexts:.java", change5);
+
+ // matching changes without extension is possible
+ assertQuery("onlyexts:txt");
+ assertQuery("onlyexts:txt,", change6);
+ assertQuery("onlyexts:,txt", change6);
+ assertQuery("onlyextensions:\"\"", change7);
+ assertQuery("onlyexts:\"\"", change7);
+ assertQuery("onlyextensions:,", change7);
+ assertQuery("onlyexts:,", change7);
+ assertFailingQuery("onlyextensions:");
+ assertFailingQuery("onlyexts:");
+
+ // inverse queries
+ assertQuery("-onlyextensions:cc,h", change7, change6, change5, change3);
+ }
+
+ @Test
+ public void byFooter() throws Exception {
+ if (getSchemaVersion() < 54) {
+ assertMissingField(ChangeField.FOOTER);
+ assertFailingQuery(
+ "footer:Change-Id=I3d2b978ed455f835d1dad2daa920be0b0ec2ae36",
+ "'footer' operator is not supported by change index version");
+ return;
+ }
+
+ TestRepository<Repo> repo = createProject("repo");
+ RevCommit commit1 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar").create());
+ Change change1 = insert(repo, newChangeForCommit(repo, commit1));
+ RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nfoo: baz").create());
+ Change change2 = insert(repo, newChangeForCommit(repo, commit2));
+ RevCommit commit3 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar\nfoo:baz").create());
+ Change change3 = insert(repo, newChangeForCommit(repo, commit3));
+ RevCommit commit4 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar=baz").create());
+ Change change4 = insert(repo, newChangeForCommit(repo, commit4));
+
+ // create a changes with lines that look like footers, but which are not
+ RevCommit commit5 =
+ repo.parseBody(
+ repo.commit().message("Test\n\nfoo: bar\n\nfoo=bar").insertChangeId().create());
+ Change change5 = insert(repo, newChangeForCommit(repo, commit5));
+ RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create());
+ insert(repo, newChangeForCommit(repo, commit6));
+
+ // matching by 'key=value' works
+ assertQuery("footer:foo=bar", change3, change1);
+ assertQuery("footer:foo=baz", change3, change2);
+ assertQuery("footer:Change-Id=" + change5.getKey(), change5);
+ assertQuery("footer:foo=bar=baz", change4);
+
+ // case doesn't matter
+ assertQuery("footer:foo=BAR", change3, change1);
+ assertQuery("footer:FOO=bar", change3, change1);
+ assertQuery("footer:fOo=BaZ", change3, change2);
+
+ // verbatim matching of footers works
+ assertQuery("footer:\"foo: bar\"", change3, change1);
+ assertQuery("footer:\"foo: baz\"", change3, change2);
+ assertQuery("footer:\"Change-Id: " + change5.getKey() + "\"", change5);
+ assertQuery("footer:\"foo: bar=baz\"", change4);
+
+ // expect no match because 'a=b: c' of commit6 is not a valid footer (footer key cannot contain
+ // '=')
+ assertQuery("footer:a=b=c");
+ assertQuery("footer:\"a=b: c\"");
+
+ // expect empty result for invalid footers
+ assertQuery("footer:foo");
+ assertQuery("footer:foo=");
+ assertQuery("footer:=foo");
+ assertQuery("footer:=");
+ }
+
+ @Test
+ public void byDirectory() throws Exception {
+ if (getSchemaVersion() < 55) {
+ assertMissingField(ChangeField.DIRECTORY);
+ String unsupportedOperatorMessage =
+ "'directory' operator is not supported by change index version";
+ assertFailingQuery("directory:src/java", unsupportedOperatorMessage);
+ assertFailingQuery("dir:src/java", unsupportedOperatorMessage);
+ return;
+ }
+
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(repo, newChangeWithFiles(repo, "src/foo.h", "src/foo.cc"));
+ Change change2 = insert(repo, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
+ Change change3 =
+ insert(repo, newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
+ Change change4 = insert(repo, newChangeWithFiles(repo, "a.txt"));
+ Change change5 = insert(repo, newChangeWithFiles(repo, "a/b/c/d/e/foo.txt"));
+
+ // matching by directory prefix works
+ assertQuery("directory:src", change2, change1);
+ assertQuery("directory:src/java", change2);
+ assertQuery("directory:src/js", change2);
+ assertQuery("directory:documentation/", change3);
+ assertQuery("directory:documentation/training", change3);
+ assertQuery("directory:documentation/training/slides", change3);
+
+ // 'dir' alias works
+ assertQuery("dir:src", change2, change1);
+ assertQuery("dir:src/java", change2);
+
+ // case doesn't matter
+ assertQuery("directory:Documentation/TrAiNiNg/SLIDES", change3);
+
+ // leading and trailing '/' doesn't matter
+ assertQuery("directory:/documentation/training/slides", change3);
+ assertQuery("directory:documentation/training/slides/", change3);
+ assertQuery("directory:/documentation/training/slides/", change3);
+
+ // files do not match as directory
+ assertQuery("directory:src/foo.h");
+ assertQuery("directory:documentation/training/slides/README.txt");
+
+ // root directory matches all changes
+ assertQuery("directory:/", change5, change4, change3, change2, change1);
+ assertQuery("directory:\"\"", change5, change4, change3, change2, change1);
+ assertFailingQuery("directory:");
+
+ // matching single directory segments works
+ assertQuery("directory:java", change2);
+ assertQuery("directory:slides", change3);
+
+ // files do not match as directory segment
+ assertQuery("directory:foo.h");
+
+ // matching any combination of intermediate directory segments works
+ assertQuery("directory:training/slides", change3);
+ assertQuery("directory:b/c", change5);
+ assertQuery("directory:b/c/d", change5);
+ assertQuery("directory:b/c/d/e", change5);
+ assertQuery("directory:c/d", change5);
+ assertQuery("directory:c/d/e", change5);
+ assertQuery("directory:d/e", change5);
+
+ // files do not match as directory segments
+ assertQuery("directory:d/e/foo.txt");
+ assertQuery("directory:e/foo.txt");
+
+ // matching any combination of intermediate directory segments works with leading and trailing
+ // '/'
+ assertQuery("directory:/b/c", change5);
+ assertQuery("directory:/b/c/", change5);
+ assertQuery("directory:b/c/", change5);
+ }
+
+ @Test
+ public void byDirectoryRegex() throws Exception {
+ assume().that(getSchemaVersion()).isAtLeast(55);
+
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(repo, newChangeWithFiles(repo, "src/java/foo.java", "src/js/bar.js"));
+ Change change2 =
+ insert(repo, newChangeWithFiles(repo, "documentation/training/slides/README.txt"));
+
+ // match by regexp
+ assertQuery("directory:^.*va.*", change1);
+ assertQuery("directory:^documentation/.*/slides", change2);
+ assertQuery("directory:^train.*", change2);
}
@Test
@@ -1648,6 +1860,24 @@
}
@Test
+ public void visibleToSelf() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(repo, newChange(repo));
+ Change change2 = insert(repo, newChange(repo));
+
+ gApi.changes().id(change2.getChangeId()).setPrivate(true, "private");
+
+ String q = "project:repo";
+ assertQuery(q + " visibleto:self", change2, change1);
+ assertQuery(q + " visibleto:me", change2, change1);
+
+ // Anonymous user cannot see first user's private change.
+ requestContext.setContext(anonymousUserProvider::get);
+ assertQuery(q + " visibleto:self", change1);
+ assertQuery(q + " visibleto:me", change1);
+ }
+
+ @Test
public void byCommentBy() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChange(repo));
@@ -2866,6 +3096,44 @@
assertQuery("project:repo+foo", change);
}
+ @Test
+ public void selfFailsForAnonymousUser() throws Exception {
+ for (String query : ImmutableList.of("assignee:self", "starredby:self", "is:starred")) {
+ assertQuery(query);
+ RequestContext oldContext = requestContext.setContext(anonymousUserProvider::get);
+
+ try {
+ requestContext.setContext(anonymousUserProvider::get);
+ assertThatAuthException(query)
+ .hasMessageThat()
+ .isEqualTo("Must be signed-in to use this operator");
+ } finally {
+ requestContext.setContext(oldContext);
+ }
+ }
+ }
+
+ @Test
+ public void selfSucceedsForInactiveAccount() throws Exception {
+ Account.Id user2 =
+ accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+
+ TestRepository<Repo> repo = createProject("repo");
+ Change change = insert(repo, newChange(repo));
+ AssigneeInput ain = new AssigneeInput();
+ ain.assignee = user2.toString();
+ gApi.changes().id(change.getId().get()).setAssignee(ain);
+
+ RequestContext adminContext = requestContext.setContext(newRequestContext(user2));
+ assertQuery("assignee:self", change);
+
+ requestContext.setContext(adminContext);
+ gApi.accounts().id(user2.get()).setActive(false);
+
+ requestContext.setContext(newRequestContext(user2));
+ assertQuery("assignee:self", change);
+ }
+
protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception {
return newChange(repo, null, null, null, null, false);
}
@@ -2967,7 +3235,6 @@
PatchSetInserter inserter =
patchSetFactory
.create(changeNotesFactory.createChecked(c), new PatchSet.Id(c.getId(), n), commit)
- .setNotify(NotifyHandling.NONE)
.setFireRevisionCreated(false)
.setValidate(false);
try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.nowTs());
@@ -2975,6 +3242,7 @@
ObjectReader reader = oi.newReader();
RevWalk rw = new RevWalk(reader)) {
bu.setRepository(repo.getRepository(), rw, oi);
+ bu.setNotify(NotifyResolver.Result.none());
bu.addOp(c.getId(), inserter);
bu.execute();
}
@@ -2995,6 +3263,15 @@
}
}
+ protected ThrowableSubject assertThatAuthException(Object query) throws Exception {
+ try {
+ newQuery(query).get();
+ throw new AssertionError("expected AuthException for query: " + query);
+ } catch (AuthException e) {
+ return assertThat(e);
+ }
+ }
+
protected TestRepository<Repo> createProject(String name) throws Exception {
gApi.projects().create(name).get();
return new TestRepository<>(repoManager.openRepository(new Project.NameKey(name)));
@@ -3126,12 +3403,19 @@
.isFalse();
}
- protected void assertFailingQuery(String query, String expectedMessage) throws Exception {
+ protected void assertFailingQuery(String query) throws Exception {
+ assertFailingQuery(query, null);
+ }
+
+ protected void assertFailingQuery(String query, @Nullable String expectedMessage)
+ throws Exception {
try {
assertQuery(query);
fail("expected BadRequestException for query '" + query + "'");
} catch (BadRequestException e) {
- assertThat(e.getMessage()).isEqualTo(expectedMessage);
+ if (expectedMessage != null) {
+ assertThat(e.getMessage()).isEqualTo(expectedMessage);
+ }
}
}
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index 5c828ba..d3c7809 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -20,6 +20,7 @@
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.access.AccessSectionInfo;
import com.google.gerrit.extensions.api.access.PermissionInfo;
@@ -48,6 +49,7 @@
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.schema.SchemaCreator;
import com.google.gerrit.server.util.ManualRequestContext;
@@ -62,6 +64,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
+import java.util.Set;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
@@ -93,6 +96,8 @@
@Inject protected AllProjectsName allProjects;
+ @Inject protected AllUsersName allUsers;
+
protected LifecycleManager lifecycle;
protected Injector injector;
protected AccountInfo currentUserInfo;
@@ -160,6 +165,28 @@
}
@Test
+ public void byParent() throws Exception {
+ assertQuery("parent:project");
+ ProjectInfo parent = createProject(name("parent"));
+ assertQuery("parent:" + parent.name);
+ ProjectInfo child = createProject(name("child"), parent.name);
+ assertQuery("parent:" + parent.name, child);
+ }
+
+ @Test
+ public void byParentOfAllProjects() throws Exception {
+ Set<String> excludedProjects = ImmutableSet.of(allProjects.get(), allUsers.get());
+ ProjectInfo[] projects =
+ gApi.projects()
+ .list()
+ .get()
+ .stream()
+ .filter(p -> !excludedProjects.contains(p.name))
+ .toArray(s -> new ProjectInfo[s]);
+ assertQuery("parent:" + allProjects.get(), projects);
+ }
+
+ @Test
public void byInname() throws Exception {
String namePart = getSanitizedMethodName();
namePart = CharMatcher.is('_').removeFrom(namePart);
@@ -296,6 +323,13 @@
return gApi.projects().create(in).get();
}
+ protected ProjectInfo createProject(String name, String parent) throws Exception {
+ ProjectInput in = new ProjectInput();
+ in.name = name;
+ in.parent = parent;
+ return gApi.projects().create(in).get();
+ }
+
protected ProjectInfo createProjectWithDescription(String name, String description)
throws Exception {
ProjectInput in = new ProjectInput();
diff --git a/javatests/com/google/gerrit/server/rules/GerritCommonTest.java b/javatests/com/google/gerrit/server/rules/GerritCommonTest.java
index 086dd65..180c16b 100644
--- a/javatests/com/google/gerrit/server/rules/GerritCommonTest.java
+++ b/javatests/com/google/gerrit/server/rules/GerritCommonTest.java
@@ -49,7 +49,7 @@
bind(PrologEnvironment.Args.class)
.toInstance(
new PrologEnvironment.Args(
- null, null, null, null, null, null, null, cfg, null, null));
+ null, null, null, null, null, null, null, null, cfg, null, null));
}
});
}
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
index d0c179b..b23219b 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaUpdaterTest.java
@@ -191,26 +191,16 @@
}
}
- private static class TestSchema_10 implements NoteDbSchemaVersion {
- @SuppressWarnings("unused")
- TestSchema_10(Arguments args) {
- // Do nothing.
- }
-
+ static class TestSchema_10 implements NoteDbSchemaVersion {
@Override
- public void upgrade(UpdateUI ui) {
+ public void upgrade(Arguments args, UpdateUI ui) {
ui.message("body of 10");
}
}
- private static class TestSchema_11 implements NoteDbSchemaVersion {
- @SuppressWarnings("unused")
- TestSchema_11(Arguments args) {
- // Do nothing.
- }
-
+ static class TestSchema_11 implements NoteDbSchemaVersion {
@Override
- public void upgrade(UpdateUI ui) {
+ public void upgrade(Arguments args, UpdateUI ui) {
ui.message("BODY OF 11");
}
}
diff --git a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
index 042ac30..530010f 100644
--- a/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
+++ b/javatests/com/google/gerrit/server/schema/NoteDbSchemaVersionsTest.java
@@ -68,9 +68,8 @@
@Test
public void schemaConstructors() throws Exception {
- NoteDbSchemaVersion.Arguments args = new NoteDbSchemaVersion.Arguments(null, null);
for (int version : NoteDbSchemaVersions.ALL.keySet()) {
- NoteDbSchemaVersions.get(NoteDbSchemaVersions.ALL, version, args);
+ NoteDbSchemaVersions.get(NoteDbSchemaVersions.ALL, version);
}
}
}
diff --git a/lib/LICENSE-resemblejs b/lib/LICENSE-resemblejs
new file mode 100644
index 0000000..b265c8a
--- /dev/null
+++ b/lib/LICENSE-resemblejs
@@ -0,0 +1,18 @@
+The MIT License (MIT) Copyright © 2013 Huddle
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the “Software”), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/lib/js/BUILD b/lib/js/BUILD
index f73b9ec..7478ef3 100644
--- a/lib/js/BUILD
+++ b/lib/js/BUILD
@@ -45,3 +45,8 @@
name = "codemirror-minified",
license = "//lib:LICENSE-codemirror-minified",
)
+
+bower_component(
+ name = "resemblejs",
+ license = "//lib:LICENSE-resemblejs",
+)
diff --git a/plugins/BUILD b/plugins/BUILD
index 2469d96..5e5dbbf 100644
--- a/plugins/BUILD
+++ b/plugins/BUILD
@@ -38,6 +38,7 @@
"//java/com/google/gerrit/index:query_exception",
"//java/com/google/gerrit/json",
"//java/com/google/gerrit/lifecycle",
+ "//java/com/google/gerrit/mail",
"//java/com/google/gerrit/metrics",
"//java/com/google/gerrit/metrics/dropwizard",
"//java/com/google/gerrit/reviewdb:server",
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 22342a6..c4cf42b 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 22342a6da26c75b14bc629331c339d1b820b4d39
+Subproject commit c4cf42b96a049a0fb854bcbcb85b56a82d91a009
diff --git a/plugins/delete-project b/plugins/delete-project
index e4c8708..189b926 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit e4c8708d14af2590607701abbce2ce16f7a370f1
+Subproject commit 189b92641c3adf6fa5ab89a7516721be75cec7e2
diff --git a/plugins/gitiles b/plugins/gitiles
index 5c911f3..623105f 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 5c911f37e429e6a5006e2d0fae2aa146ce5cb7b1
+Subproject commit 623105f14dca02cb294ed94a952f5e8ce0e96683
diff --git a/plugins/webhooks b/plugins/webhooks
index edf3122..0629027 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit edf3122969d445b65feef2174828e115fb1ceedc
+Subproject commit 062902794ff684c91eac5b860d3c488354997a21
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 d4e83d9..7e68669 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
@@ -232,7 +232,8 @@
return restAPI.getChangeDetail(change._number)
.then(detail => {
if (!detail) {
- return Promise.reject('Unable to check for latest patchset.');
+ const error = new Error('Unable to check for latest patchset.');
+ return Promise.reject(error);
}
const actualLatest = Gerrit.PatchSetBehavior.computeLatestPatchNum(
Gerrit.PatchSetBehavior.computeAllPatchSets(detail));
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 81123e7..b86d971 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
@@ -233,9 +233,10 @@
const memberName = 'bad-name';
const alertStub = sandbox.stub();
element.addEventListener('show-alert', alertStub);
-
+ const error = new Error('error');
+ error.status = 404;
sandbox.stub(element.$.restAPI, 'saveGroupMembers',
- () => Promise.reject({status: 404}));
+ () => Promise.reject(error));
element.$.groupMemberSearchInput.text = memberName;
element.$.groupMemberSearchInput.value = 1234;
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
index 22f461b..a5bb5fd 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.html
@@ -109,6 +109,7 @@
items="{{_rules}}"
as="rule">
<gr-rule-editor
+ has-range="[[_computeHasRange(name)]]"
label="[[_label]]"
editing="[[editing]]"
group-id="[[rule.id]]"
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
index 31d371d..afe5a86 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -19,6 +19,11 @@
const MAX_AUTOCOMPLETE_RESULTS = 20;
+ const RANGE_NAMES = [
+ 'QUERY LIMIT',
+ 'BATCH CHANGES LIMIT',
+ ];
+
/**
* Fired when the permission has been modified or removed.
*
@@ -269,5 +274,11 @@
this.set(['permission', 'value', 'rules', groupId], value);
this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
},
+
+ _computeHasRange(name) {
+ if (!name) { return false; }
+
+ return RANGE_NAMES.includes(name.toUpperCase());
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
index e29c4a2..a6381d1 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -255,6 +255,14 @@
assert.isFalse(element._deleted);
assert.isNotOk(element.permission.value.deleted);
});
+
+ test('_computeHasRange', () => {
+ assert.isTrue(element._computeHasRange('Query Limit'));
+
+ assert.isTrue(element._computeHasRange('Batch Changes Limit'));
+
+ assert.isFalse(element._computeHasRange('test'));
+ });
});
suite('interactions', () => {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
index 77a1e2a..d79bf0d 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -307,8 +307,8 @@
commands.push({
title,
command: commandObj[title]
- .replace('${project}', encodeURI(repo))
- .replace('${project-base-name}',
+ .replace(/\$\{project\}/gi, encodeURI(repo))
+ .replace(/\$\{project-base-name\}/gi,
encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
});
}
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
index d59deed4..c8ae650 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.html
@@ -71,7 +71,11 @@
color: var(--deemphasized-text-color);
}
</style>
- <style include="gr-form-styles"></style>
+ <style include="gr-form-styles">
+ iron-autogrow-textarea {
+ width: 14em;
+ }
+ </style>
<div id="mainContainer"
class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]">
<div id="options">
@@ -106,6 +110,22 @@
</select>
</gr-select>
</template>
+ <template is="dom-if" if="[[hasRange]]">
+ <iron-autogrow-textarea
+ id="minInput"
+ class="min"
+ autocomplete="on"
+ placeholder="Min value"
+ bind-value="{{rule.value.min}}"
+ disabled$="[[!editing]]"></iron-autogrow-textarea>
+ <iron-autogrow-textarea
+ id="maxInput"
+ class="max"
+ autocomplete="on"
+ placeholder="Max value"
+ bind-value="{{rule.value.max}}"
+ disabled$="[[!editing]]"></iron-autogrow-textarea>
+ </template>
<a class="groupPath" href$="[[_computeGroupPath(groupId)]]">
[[groupName]]
</a>
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index b99125c..06f703f 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -67,6 +67,7 @@
is: 'gr-rule-editor',
properties: {
+ hasRange: Boolean,
/** @type {?} */
label: Object,
editing: {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 708a730..0ce622546 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -226,6 +226,7 @@
_computeItemNeedsReview(account, change, showReviewedState) {
return showReviewedState && !change.reviewed &&
+ !change.work_in_progress &&
this.changeIsOpen(change.status) &&
(!account || account._account_id != change.owner._account_id);
},
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index d20d40a..d5b9aa9 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -231,11 +231,17 @@
status: 'ABANDONED',
owner: {_account_id: 0},
},
+ {
+ _number: 4,
+ status: 'NEW',
+ work_in_progress: true,
+ owner: {_account_id: 0},
+ },
];
flushAsynchronousOperations();
let elementItems = Polymer.dom(element.root).querySelectorAll(
'gr-change-list-item');
- assert.equal(elementItems.length, 4);
+ assert.equal(elementItems.length, 5);
for (let i = 0; i < elementItems.length; i++) {
assert.isFalse(elementItems[i].hasAttribute('needs-review'));
}
@@ -243,20 +249,22 @@
element.showReviewedState = true;
elementItems = Polymer.dom(element.root).querySelectorAll(
'gr-change-list-item');
- assert.equal(elementItems.length, 4);
+ assert.equal(elementItems.length, 5);
assert.isFalse(elementItems[0].hasAttribute('needs-review'));
assert.isTrue(elementItems[1].hasAttribute('needs-review'));
assert.isFalse(elementItems[2].hasAttribute('needs-review'));
assert.isFalse(elementItems[3].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[4].hasAttribute('needs-review'));
element.account = {_account_id: 42};
elementItems = Polymer.dom(element.root).querySelectorAll(
'gr-change-list-item');
- assert.equal(elementItems.length, 4);
+ assert.equal(elementItems.length, 5);
assert.isFalse(elementItems[0].hasAttribute('needs-review'));
assert.isTrue(elementItems[1].hasAttribute('needs-review'));
assert.isFalse(elementItems[2].hasAttribute('needs-review'));
assert.isFalse(elementItems[3].hasAttribute('needs-review'));
+ assert.isFalse(elementItems[4].hasAttribute('needs-review'));
});
test('no changes', () => {
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 89d4f1f..c41d4e4 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
@@ -64,7 +64,9 @@
});
},
send(method, url, payload) {
- if (method !== 'POST') { return Promise.reject('bad method'); }
+ if (method !== 'POST') {
+ return Promise.reject(new Error('bad method'));
+ }
if (url === '/changes/test~42/revisions/2/submit') {
return Promise.resolve({
@@ -78,7 +80,7 @@
});
}
- return Promise.reject('bad url');
+ return Promise.reject(new Error('bad url'));
},
getProjectConfig() { return Promise.resolve({}); },
});
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 7bb4656..cc55974 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -622,7 +622,7 @@
return Promise.resolve();
} else {
this._redirectToLogin(data.canonicalPath);
- return Promise.reject();
+ return Promise.reject(new Error());
}
});
},
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index 8aff87e..6699bd1 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -36,9 +36,12 @@
'conflicts:',
'deleted:',
'delta:',
+ 'dir:',
+ 'directory:',
'ext:',
'extension:',
'file:',
+ 'footer:',
'from:',
'has:',
'has:draft',
@@ -66,6 +69,8 @@
'is:wip',
'label:',
'message:',
+ 'onlyexts:',
+ 'onlyextensions:',
'owner:',
'ownerin:',
'parentproject:',
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 9fc9232..1be2cb1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -158,21 +158,6 @@
/** @type ?Defs.LineOfInterest */
lineOfInterest: Object,
- /**
- * The key locations based on the comments and line of interests,
- * where lines should not be collapsed.
- *
- * @type {{left: Object<(string|number), number>,
- * right: Object<(string|number), number>}}
- */
- _keyLocations: {
- type: Object,
- value: () => ({
- left: {},
- right: {},
- }),
- },
-
loading: {
type: Boolean,
value: false,
@@ -321,7 +306,6 @@
const addedThreadEls = info.addedNodes.filter(isThreadEl);
const removedThreadEls = info.removedNodes.filter(isThreadEl);
this._updateRanges(addedThreadEls, removedThreadEls);
- this._updateKeyLocations(addedThreadEls, removedThreadEls);
this._redispatchHoverEvents(addedThreadEls);
});
},
@@ -349,17 +333,29 @@
this.push('_commentRanges', ...addedCommentRanges);
},
- _updateKeyLocations(addedThreadEls, removedThreadEls) {
- for (const threadEl of addedThreadEls) {
- const commentSide = threadEl.getAttribute('comment-side');
- const lineNum = threadEl.getAttribute('line-num') || GrDiffLine.FILE;
- this._keyLocations[commentSide][lineNum] = true;
+ /**
+ * The key locations based on the comments and line of interests,
+ * where lines should not be collapsed.
+ *
+ * @return {{left: Object<(string|number), boolean>,
+ * right: Object<(string|number), boolean>}}
+ */
+ _computeKeyLocations() {
+ const keyLocations = {left: {}, right: {}};
+ if (this.lineOfInterest) {
+ const side = this.lineOfInterest.leftSide ? 'left' : 'right';
+ keyLocations[side][this.lineOfInterest.number] = true;
}
- for (const threadEl of removedThreadEls) {
+ const threadEls = Polymer.dom(this).getEffectiveChildNodes()
+ .filter(isThreadEl);
+
+ for (const threadEl of threadEls) {
const commentSide = threadEl.getAttribute('comment-side');
- const lineNum = threadEl.getAttribute('line-num') || GrDiffLine.FILE;
- this._keyLocations[commentSide][lineNum] = false;
+ const lineNum = Number(threadEl.getAttribute('line-num')) ||
+ GrDiffLine.FILE;
+ keyLocations[commentSide][lineNum] = true;
}
+ return keyLocations;
},
// Dispatch events that are handled by the gr-diff-highlight.
@@ -691,11 +687,8 @@
this._showWarning = false;
- if (this.lineOfInterest) {
- const side = this.lineOfInterest.leftSide ? 'left' : 'right';
- this._keyLocations[side][this.lineOfInterest.number] = true;
- }
- this.$.diffBuilder.render(this._keyLocations, this._getBypassPrefs());
+ const keyLocations = this._computeKeyLocations();
+ this.$.diffBuilder.render(keyLocations, this._getBypassPrefs());
},
_handleRenderContent() {
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index f26165b..bc072b1 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -22,6 +22,7 @@
'application/json': 'json',
'application/x-powershell': 'powershell',
'application/typescript': 'typescript',
+ 'application/xml': 'xml',
'application/xquery': 'xquery',
'application/x-erb': 'erb',
'text/css': 'css',
@@ -65,6 +66,7 @@
'text/x-perl': 'perl',
'text/x-pgsql': 'pgsql', // postgresql
'text/x-php': 'php',
+ 'text/x-properties': 'properties',
'text/x-protobuf': 'protobuf',
'text/x-puppet': 'puppet',
'text/x-python': 'python',
@@ -79,9 +81,11 @@
'text/x-sh': 'bash',
'text/x-sql': 'sql',
'text/x-swift': 'swift',
+ 'text/x-systemverilog': 'sv',
'text/x-tcl': 'tcl',
'text/x-twig': 'twig',
'text/x-vb': 'vb',
+ 'text/x-verilog': 'v',
'text/x-yaml': 'yaml',
'text/vbscript': 'vbscript',
};
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index 5006461..70eed78 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -29,6 +29,16 @@
type: Map,
value() { return new Map(); },
},
+ /**
+ * This map prevents importing the same endpoint twice.
+ * Without caching, if a plugin is loaded after the loaded plugins
+ * callback fires, it will be imported twice and appear twice on the page.
+ * @type {!Map}
+ */
+ _initializedPlugins: {
+ type: Map,
+ value() { return new Map(); },
+ },
},
detached() {
@@ -102,6 +112,9 @@
},
_initModule({moduleName, plugin, type, domHook}) {
+ if (this._initializedPlugins.get(plugin.name)) {
+ return;
+ }
let initPromise;
switch (type) {
case 'decorate':
@@ -115,6 +128,7 @@
console.warn('Unable to initialize module' +
`${moduleName} from ${plugin.getPluginName()}`);
}
+ this._initializedPlugins.set(plugin.name, true);
initPromise.then(el => {
domHook.handleInstanceAttached(el);
this._domHooks.set(el, domHook);
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
index 13b3152..0a2ae78 100644
--- 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
@@ -169,7 +169,7 @@
const newKeyString = 'not even close to valid';
const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey',
- () => { return Promise.reject(); });
+ () => { return Promise.reject(new Error('error')); });
element._newKey = newKeyString;
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
index 8607948..1785d1f 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
@@ -155,7 +155,7 @@
const newKeyString = 'not even close to valid';
const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey',
- () => { return Promise.reject(); });
+ () => { return Promise.reject(new Error('error')); });
element._newKey = newKeyString;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index 8efd309..3afbe54 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -26,8 +26,8 @@
};
const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' +
- 'It will not appear in dashboards, and email notifications will be ' +
- 'silenced until the review is started.';
+ 'It will not appear on dashboards unless you are CC\'ed or assigned, ' +
+ 'and email notifications will be silenced until the review is started.';
const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
'current reviewers (or anyone with "View Private Changes" permission).';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
index bf6a046..ca87956 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
@@ -135,7 +135,7 @@
__key: 'key',
__url: '/changes/1/revisions/2/foo~bar',
};
- const sendStub = sandbox.stub().returns(Promise.reject('boom'));
+ const sendStub = sandbox.stub().returns(Promise.reject(new Error('boom')));
sandbox.stub(plugin, 'restApi').returns({
send: sendStub,
});
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 65f1f40..2c4404d 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
@@ -1896,7 +1896,7 @@
const query = [
'status:open',
'-change:' + changeNum,
- 'topic:' + topic,
+ `topic:"${topic}"`,
].join(' ');
const params = {
O: options,
@@ -2506,7 +2506,9 @@
_fetchB64File(url) {
return this._fetch({url: this.getBaseUrl() + url})
.then(response => {
- if (!response.ok) { return Promise.reject(response.statusText); }
+ if (!response.ok) {
+ return Promise.reject(new Error(response.statusText));
+ }
const type = response.headers.get('X-FYI-Content-Type');
return response.text()
.then(text => {
@@ -2666,12 +2668,12 @@
return this._send(req)
.then(response => {
if (response.status < 200 && response.status >= 300) {
- return Promise.reject();
+ return Promise.reject(new Error('error'));
}
return this.getResponseObject(response);
})
.then(obj => {
- if (!obj.valid) { return Promise.reject(); }
+ if (!obj.valid) { return Promise.reject(new Error('error')); }
return obj;
});
},
@@ -2701,12 +2703,12 @@
return this._send(req)
.then(response => {
if (response.status < 200 && response.status >= 300) {
- return Promise.reject();
+ return Promise.reject(new Error('error'));
}
return this.getResponseObject(response);
})
.then(obj => {
- if (!obj) { return Promise.reject(); }
+ if (!obj) { return Promise.reject(new Error('error')); }
return obj;
});
},
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 5d4a3b0..4ab1e04 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
@@ -88,10 +88,10 @@
});
test('cached promise', done => {
- const promise = Promise.reject('foo');
+ const promise = Promise.reject(new Error('foo'));
element._cache.set('/foo', promise);
element._fetchSharedCacheURL({url: '/foo'}).catch(p => {
- assert.equal(p, 'foo');
+ assert.equal(p.message, 'foo');
done();
});
});
@@ -455,7 +455,7 @@
status: 403,
};
window.fetch.onFirstCall().returns(
- Promise.reject({message: 'Failed to fetch'}));
+ Promise.reject(new Error('Failed to fetch')));
window.fetch.onSecondCall().returns(Promise.resolve(fakeAuthResponse));
// Emulate logged in.
element._cache.set('/accounts/self/detail', {});
@@ -507,7 +507,7 @@
element._cache.set('/accounts/self/detail', true);
sandbox.spy(element, 'checkCredentials');
sandbox.stub(window, 'fetch', url => {
- return Promise.reject({message: 'Failed to fetch'});
+ return Promise.reject(new Error('Failed to fetch'));
});
return element.getConfig(true)
.catch(err => undefined)
diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
index 78c8684..9801b44 100644
--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -33,6 +33,10 @@
<meta name="referrer" content="never">{\n}
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">{\n}
+ <noscript>
+ To use PolyGerrit, please enable JavaScript in your browser settings, and then refresh this page.
+ </noscript>
+
<script>
window.CLOSURE_NO_DEPS = true;
{if $canonicalPath != ''}window.CANONICAL_PATH = '{$canonicalPath}';{/if}
diff --git a/resources/com/google/gerrit/server/change/ChangeMessages.properties b/resources/com/google/gerrit/server/change/ChangeMessages.properties
index 7c1dce3..ec20677 100644
--- a/resources/com/google/gerrit/server/change/ChangeMessages.properties
+++ b/resources/com/google/gerrit/server/change/ChangeMessages.properties
@@ -1,9 +1,7 @@
revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}.
reviewerCantSeeChange = {0} does not have permission to see this change
-reviewerInactive = {0} identifies an inactive account
reviewerInvalid = {0} is not a valid user identifier
-reviewerNotFoundUser = {0} does not identify a registered user
reviewerNotFoundUserOrGroup = {0} does not identify a registered user or group
groupIsNotAllowed = The group {0} cannot be added as reviewer.
diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties
index 77d6f0f..af5ba65 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -210,6 +210,8 @@
ss = text/x-scheme
st = text/x-stsrc
stex = text/x-stex
+sv = x-systemverilog
+svh = x-systemverilog
swift = text/x-swift
tcl = text/x-tcl
tex = text/x-latex