Merge "Allow labelAs-$NAME permission for InternalUser"
diff --git a/BUILD b/BUILD
index 7ae3589..4fa30f2 100644
--- a/BUILD
+++ b/BUILD
@@ -8,6 +8,14 @@
visibility = ['//visibility:public'],
)
+genrule(
+ name = "LICENSES",
+ srcs = ["//Documentation:licenses.txt"],
+ cmd = "cp $< $@",
+ outs = ["LICENSES.txt"],
+ visibility = ['//visibility:public'],
+)
+
pkg_war(name = 'gerrit')
pkg_war(name = 'headless', ui = None)
pkg_war(name = 'release', ui = 'ui_optdbg_r', context = ['//plugins:core'])
diff --git a/Documentation/BUILD b/Documentation/BUILD
index c2acc9c..0542b5d 100644
--- a/Documentation/BUILD
+++ b/Documentation/BUILD
@@ -1,6 +1,46 @@
-
+load("//tools/bzl:asciidoc.bzl", "documentation_attributes")
+load("//tools/bzl:asciidoc.bzl", "genasciidoc")
+load("//tools/bzl:asciidoc.bzl", "genasciidoc_zip")
load("//tools/bzl:license.bzl", "license_map")
+exports_files([
+ "replace_macros.py",
+])
+
+filegroup(
+ name = "prettify_files",
+ srcs = [
+ ":prettify.min.css",
+ ":prettify.min.js",
+ ],
+)
+
+genrule(
+ name = "prettify_min_css",
+ srcs = ["//gerrit-prettify:src/main/resources/com/google/gerrit/prettify/client/prettify.css"],
+ cmd = "cp $< $@",
+ outs = ["prettify.min.css"],
+)
+
+genrule(
+ name = "prettify_min_js",
+ srcs = ["//gerrit-prettify:src/main/resources/com/google/gerrit/prettify/client/prettify.js"],
+ cmd = "cp $< $@",
+ outs = ["prettify.min.js"],
+)
+
+filegroup(
+ name = "resources",
+ srcs = glob([
+ "images/*.jpg",
+ "images/*.png",
+ ]) + [
+ ":prettify_files",
+ "//:LICENSES.txt",
+ ],
+ visibility = ['//visibility:public'],
+)
+
license_map(
name = "licenses",
targets = [
@@ -8,10 +48,11 @@
"//gerrit-gwtui:ui_module",
],
opts = ["--asciidoctor"],
+ visibility = ['//visibility:public'],
)
DOC_DIR = "Documentation"
-SRCS = glob(["*.txt"])
+SRCS = glob(["*.txt"]) + [":licenses.txt"]
genrule(
name = "index",
@@ -20,12 +61,38 @@
'--prefix "%s/" ' % DOC_DIR +
'--in-ext ".txt" ' +
'--out-ext ".html" ' +
- "$(SRCS) " +
- "$(location :licenses.txt)",
- tools = [
- ":licenses.txt",
- "//lib/asciidoctor:doc_indexer",
- ],
+ "$(SRCS)",
+ tools = ["//lib/asciidoctor:doc_indexer"],
srcs = SRCS,
outs = ["index.jar"],
)
+
+# For the same srcs, we can have multiple genasciidoc_zip rules, but only one
+# genasciidoc rule. Because multiple genasciidoc rules will have conflicting
+# output files.
+genasciidoc(
+ name = "Documentation",
+ srcs = SRCS,
+ attributes = documentation_attributes(),
+ backend = "html5",
+ visibility = ["//visibility:public"],
+)
+
+genasciidoc_zip(
+ name = "html",
+ srcs = SRCS,
+ attributes = documentation_attributes(),
+ backend = "html5",
+ directory = DOC_DIR,
+ visibility = ["//visibility:public"],
+)
+
+genasciidoc_zip(
+ name = "searchfree",
+ srcs = SRCS,
+ attributes = documentation_attributes(),
+ backend = "html5",
+ directory = DOC_DIR,
+ searchbox = False,
+ visibility = ["//visibility:public"],
+)
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 1c7981a..462c226 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -67,6 +67,15 @@
+
Default is 20.
+[[addReviewer.baseWeight]]addReviewer.baseWeight::
++
+The weight that will be applied in the default reviewer ranking algorithm.
+This can be increased or decreased to give more or less influence to plugins.
+If set to zero, the base ranking will not have any effect. Reviewers will then
+be ordered as ranked by the plugins (if there are any).
++
+By default 1.
+
[[auth]]
=== Section auth
@@ -3907,11 +3916,11 @@
[[suggest.from]]suggest.from::
+
The number of characters that a user must have typed before suggestions
-are provided. If set to 0, suggestions are always provided.
+are provided. If set to 0, suggestions are always provided. This is only
+used for suggesting accounts when adding members to a group.
+
By default 0.
-
[[theme]]
=== Section theme
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 5483d85..34f39c8 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -84,6 +84,11 @@
also redefine the text and behavior of the built in label types `Code-Review`
and `Verified`.
+Optionally a +commentlink+ section can be added to define project-specific
+comment links. The +commentlink+ section has the same format as the
+link:config-gerrit.html#commentlink[+commentlink+ section in gerrit.config]
+which is used to define global comment links.
+
[[project-section]]
=== Project section
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 3260e23..ccfc440 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2399,6 +2399,44 @@
----
+[[reviewer-suggestion]]
+== Reviewer Suggestion Plugins
+
+Gerrit provides an extension point that enables Plugins to rank
+the list of reviewer suggestion a user receives upon clicking "Add Reviewer" on
+the change screen.
+Gerrit supports both a default suggestion that appears when the user has not yet
+typed anything and a filtered suggestion that is shown as the user starts
+typing.
+Plugins receive a candidate list and can return a Set of suggested reviewers
+containing the Account.Id and a score for each reviewer.
+The candidate list is non-binding and plugins can choose to return reviewers not
+initially contained in the candidate list.
+Server administrators can configure the overall weight of each plugin using the
+weight config parameter on [addreviewer "<pluginName-exportName>"].
+
+[source, java]
+----
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+
+import java.util.Set;
+
+public class MyPlugin implements ReviewerSuggestion {
+ public Set<SuggestedReviewer> suggestReviewers(Project.NameKey project,
+ @Nullable Change.Id changeId, @Nullable String query,
+ Set<Account.Id> candidates) {
+ Set<SuggestedReviewer> suggestions = new HashSet<>();
+ // Implement your ranking logic here
+ return suggestions;
+ }
+}
+----
+
+
== SEE ALSO
* link:js-api.html[JavaScript API]
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 4959ced..bd7f6e9 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -90,22 +90,49 @@
java -jar buck-out/gen/gerrit/gerrit.war init -d ../gerrit_testsite
----
-Accept defaults by pressing Enter until 'init' completes, or add
-the '--batch' command line option to avoid them entirely. It is
-recommended to change the listen addresses from '*' to 'localhost' to
-prevent outside connections from contacting the development instance.
+During initialization, make two changes to the default settings:
-The daemon will automatically start in the background and a web
-browser will launch to the start page, enabling login via OpenID.
+* Change the listen addresses from '*' to 'localhost' to prevent outside
+ connections from contacting the development instance; and
+* Change the auth type from 'OPENID' to 'DEVELOPMENT_BECOME_ANY_ACCOUNT' to
+ allow yourself to create and act as arbitrary test accounts on your
+ development instance.
-Shutdown the daemon after registering the administrator account
-through the web interface:
+Continue through init until it completes. The daemon will automatically start in
+the background and a web browser will launch to the start page. From here you
+can sign in as the account created during init, register additional accounts,
+create projects, and more.
+
+When you want to shut down the daemon, simply run:
----
../gerrit_testsite/bin/gerrit.sh stop
----
+[[localdev]]
+== Working with the Local Server
+
+If you need to create additional accounts on your development instance, click
+'become' in the upper right corner, select 'Switch User', and then register
+a new account.
+
+Use the `ssh` protocol to clone from and push to the local server. For
+example, to clone a repository that you've created through the admin
+interface, run:
+
+----
+git clone ssh://username@localhost:29418/projectname
+----
+
+Then you'll be able to create changes the same way users do, with
+
+----
+git push origin HEAD:refs/for/master
+----
+
+
+
== Testing
diff --git a/Documentation/intro-project-owner.txt b/Documentation/intro-project-owner.txt
index 7a724f7..72fe717 100644
--- a/Documentation/intro-project-owner.txt
+++ b/Documentation/intro-project-owner.txt
@@ -70,8 +70,8 @@
commands:
----
- $ git fetch origin refs/meta/config:config
- $ git checkout config
+ $ git fetch ssh://localhost:29418/project refs/meta/config
+ $ git checkout FETCH_HEAD
$ git log project.config
----
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index 39167bd..9a16cdf 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -10,6 +10,7 @@
_java_ -jar gerrit.war _init_
-d <SITE_PATH>
[--batch]
+ [--delete-caches]
[--no-auto-start]
[--skip-plugins]
[--list-plugins]
@@ -42,6 +43,10 @@
statements to drop these objects is provided. To drop the unused
objects these SQL statements must be executed manually.
+--delete-caches::
+ Force deletion of all persistent cache files. Note that
+ re-creation of these caches may be expensive.
+
--no-auto-start::
Don't automatically start the daemon after initializing a
newly created site path. This permits the administrator
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index e7a1cdf..32e93d3 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -4860,11 +4860,13 @@
=== CherryPickInput
The `CherryPickInput` entity contains information for cherry-picking a change to a new branch.
-[options="header",cols="1,6"]
+[options="header",cols="1,^1,5"]
|===========================
-|Field Name |Description
-|`message` |Commit message for the cherry-picked change
-|`destination` |Destination branch
+|Field Name ||Description
+|`message` ||Commit message for the cherry-picked change
+|`destination` ||Destination branch
+|`parent` |optional, defaults to 1|
+Number of the parent relative to which the cherry-pick should be considered.
|===========================
[[comment-info]]
@@ -5579,7 +5581,9 @@
Allowed values are `DELETE`, `PUBLISH`, `PUBLISH_ALL_REVISIONS` and
`KEEP`. All values except `PUBLISH_ALL_REVISIONS` operate only on drafts
for a single revision. +
-If not set, the default is `DELETE`.
+Only `KEEP` is allowed when used in conjunction with `on_behalf_of`. +
+If not set, the default is `DELETE`, unless `on_behalf_of` is set, in
+which case the default is `KEEP` and any other value is disallowed.
|`notify` |optional|
Notify handling that defines to whom email notifications should be sent
after the review is stored. +
@@ -5710,6 +5714,8 @@
|`robot_id` ||The ID of the robot that generated this comment.
|`robot_run_id` ||An ID of the run of the robot.
|`url` |optional|URL to more information.
+|`properties` |optional|
+Robot specific properties as map that maps arbitrary keys to values.
|===========================
[[robot-comment-input]]
diff --git a/ReleaseNotes/BUILD b/ReleaseNotes/BUILD
new file mode 100644
index 0000000..9bf572e
--- /dev/null
+++ b/ReleaseNotes/BUILD
@@ -0,0 +1,27 @@
+load("//tools/bzl:asciidoc.bzl", "release_notes_attributes")
+load("//tools/bzl:asciidoc.bzl", "genasciidoc")
+load("//tools/bzl:asciidoc.bzl", "genasciidoc_zip")
+
+
+SRCS = glob(['*.txt'])
+
+
+genasciidoc(
+ name = 'ReleaseNotes',
+ srcs = SRCS,
+ attributes = release_notes_attributes(),
+ backend = 'html5',
+ searchbox = False,
+ resources = False,
+ visibility = ["//visibility:public"],
+)
+
+genasciidoc_zip(
+ name = "html",
+ srcs = SRCS,
+ attributes = release_notes_attributes(),
+ backend = 'html5',
+ searchbox = False,
+ resources = False,
+ visibility = ["//visibility:public"],
+)
diff --git a/WORKSPACE b/WORKSPACE
index 6660924..4487821 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -672,8 +672,8 @@
maven_jar(
name = 'truth',
- artifact = 'com.google.truth:truth:0.28',
- sha1 = '0a388c7877c845ff4b8e19689dda5ac9d34622c4',
+ artifact = 'com.google.truth:truth:0.30',
+ sha1 = '9d591b5a66eda81f0b88cf1c748ab8853d99b18b',
)
maven_jar(
@@ -962,6 +962,14 @@
name = "bower",
)
+npm_binary(
+ name = "vulcanize",
+)
+
+npm_binary(
+ name = "crisper",
+)
+
# bower_archive() seed components.
bower_archive(
name = 'iron-autogrow-textarea',
@@ -1040,6 +1048,29 @@
sha1 = 'a3b598c06cbd7f441402e666ff748326030905d6',
)
+# bower test stuff
+
+bower_archive(
+ name = 'iron-test-helpers',
+ package = 'polymerelements/iron-test-helpers',
+ version = '1.2.5',
+ sha1 = '433b03b106f5ff32049b84150cd70938e18b67ac',
+)
+
+bower_archive(
+ name = 'test-fixture',
+ package = 'polymerelements/test-fixture',
+ version = '1.1.1',
+ sha1 = 'e373bd21c069163c3a754e234d52c07c77b20d3c',
+)
+
+bower_archive(
+ name = 'web-component-tester',
+ package = 'web-component-tester',
+ version = '4.2.2',
+ sha1 = '54556000c33d9ed7949aa546c1b4a1531491a5f0',
+)
+
# Bower component transitive dependencies.
load("//lib/js:bower_archives.bzl", "load_bower_archives")
load_bower_archives()
diff --git a/contrib/populate-fixture-data.py b/contrib/populate-fixture-data.py
index c35f82c..b77c41a 100644
--- a/contrib/populate-fixture-data.py
+++ b/contrib/populate-fixture-data.py
@@ -182,14 +182,15 @@
def get_random_users(num_users):
- users = [(f, l) for f in FIRST_NAMES for l in LAST_NAMES][:num_users]
+ users = random.sample([(f, l) for f in FIRST_NAMES for l in LAST_NAMES],
+ num_users)
names = []
for u in users:
names.append({"firstname": u[0],
"lastname": u[1],
"name": u[0] + " " + u[1],
"username": u[0] + u[1],
- "email": u[0] + "." + u[1] + "@gmail.com",
+ "email": u[0] + "." + u[1] + "@gerritcodereview.com",
"http_password": "secret",
"groups": []})
return names
@@ -293,6 +294,7 @@
project_names = create_gerrit_projects(group_names)
for idx, u in enumerate(gerrit_users):
- create_change(u, project_names[4 * idx / len(gerrit_users)])
+ for _ in xrange(random.randint(1, 5)):
+ create_change(u, project_names[4 * idx / len(gerrit_users)])
main()
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index bce0b5a..114ef6a 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -14,6 +14,7 @@
package com.google.gerrit.acceptance;
+import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.US_ASCII;
@@ -104,6 +105,7 @@
for (String n : groups) {
AccountGroup.NameKey k = new AccountGroup.NameKey(n);
AccountGroup g = groupCache.get(k);
+ checkArgument(g != null, "group not found: %s", n);
AccountGroupMember m =
new AccountGroupMember(new AccountGroupMember.Key(id, g.getId()));
db.accountGroupMembers().insert(Collections.singleton(m));
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
index 669b991..e5182df 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance;
import com.google.common.base.CharMatcher;
+import com.google.gerrit.common.Nullable;
import org.apache.http.HttpHost;
import org.apache.http.client.fluent.Executor;
@@ -24,17 +25,20 @@
import java.net.URI;
public class HttpSession {
-
+ protected TestAccount account;
protected final String url;
private final Executor executor;
- public HttpSession(GerritServer server, TestAccount account) {
+ public HttpSession(GerritServer server, @Nullable TestAccount account) {
this.url = CharMatcher.is('/').trimTrailingFrom(server.getUrl());
URI uri = URI.create(url);
- this.executor = Executor
- .newInstance()
- .auth(new HttpHost(uri.getHost(), uri.getPort()),
+ this.executor = Executor.newInstance();
+ this.account = account;
+ if (account != null) {
+ executor.auth(
+ new HttpHost(uri.getHost(), uri.getPort()),
account.username, account.httpPassword);
+ }
}
public String url() {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
index 90ece46..689b2d0 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -18,6 +18,7 @@
import com.google.common.base.Preconditions;
import com.google.common.net.HttpHeaders;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.server.OutputFormat;
@@ -32,7 +33,7 @@
public class RestSession extends HttpSession {
- public RestSession(GerritServer server, TestAccount account) {
+ public RestSession(GerritServer server, @Nullable TestAccount account) {
super(server, account);
}
@@ -47,7 +48,7 @@
public RestResponse getWithHeader(String endPoint, Header header)
throws IOException {
- Request get = Request.Get(url + "/a" + endPoint);
+ Request get = Request.Get(getUrl(endPoint));
if (header != null) {
get.addHeader(header);
}
@@ -55,7 +56,7 @@
}
public RestResponse head(String endPoint) throws IOException {
- return execute(Request.Head(url + "/a" + endPoint));
+ return execute(Request.Head(getUrl(endPoint)));
}
public RestResponse put(String endPoint) throws IOException {
@@ -73,7 +74,7 @@
public RestResponse putWithHeader(String endPoint, Header header,
Object content) throws IOException {
- Request put = Request.Put(url + "/a" + endPoint);
+ Request put = Request.Put(getUrl(endPoint));
if (header != null) {
put.addHeader(header);
}
@@ -88,7 +89,7 @@
public RestResponse putRaw(String endPoint, RawInput stream) throws IOException {
Preconditions.checkNotNull(stream);
- Request put = Request.Put(url + "/a" + endPoint);
+ Request put = Request.Put(getUrl(endPoint));
put.addHeader(new BasicHeader("Content-Type", stream.getContentType()));
put.body(new BufferedHttpEntity(
new InputStreamEntity(
@@ -102,7 +103,15 @@
}
public RestResponse post(String endPoint, Object content) throws IOException {
- Request post = Request.Post(url + "/a" + endPoint);
+ return postWithHeader(endPoint, content, null);
+ }
+
+ public RestResponse postWithHeader(String endPoint, Object content,
+ Header header) throws IOException {
+ Request post = Request.Post(getUrl(endPoint));
+ if (header != null) {
+ post.addHeader(header);
+ }
if (content != null) {
post.addHeader(new BasicHeader("Content-Type", "application/json"));
post.body(new StringEntity(
@@ -113,6 +122,10 @@
}
public RestResponse delete(String endPoint) throws IOException {
- return execute(Request.Delete(url + "/a" + endPoint));
+ return execute(Request.Delete(getUrl(endPoint)));
+ }
+
+ private String getUrl(String endPoint) {
+ return url + (account != null ? "/a" : "") + endPoint;
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 2475efa..3dd1ff3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -415,37 +415,6 @@
}
@Test
- public void voteOnBehalfOf() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- LabelType codeReviewType = Util.codeReview();
- String forCodeReviewAs = Permission.forLabelAs(codeReviewType.getName());
- String heads = "refs/heads/*";
- AccountGroup.UUID owner =
- SystemGroupBackend.getGroup(CHANGE_OWNER).getUUID();
- Util.allow(cfg, forCodeReviewAs, -1, 1, owner, heads);
- saveProjectConfig(project, cfg);
-
- PushOneCommit.Result r = createChange();
- RevisionApi revision = gApi.changes()
- .id(r.getChangeId())
- .current();
-
- ReviewInput in = ReviewInput.recommend();
- in.onBehalfOf = user.id.toString();
- revision.review(in);
-
- ChangeInfo c = gApi.changes()
- .id(r.getChangeId())
- .get();
-
- LabelInfo codeReview = c.labels.get("Code-Review");
- assertThat(codeReview.all).hasSize(1);
- ApprovalInfo approval = codeReview.all.get(0);
- assertThat(approval._accountId).isEqualTo(user.id.get());
- assertThat(approval.value).isEqualTo(1);
- }
-
- @Test
public void rebaseUpToDateChange() throws Exception {
PushOneCommit.Result r = createChange();
exception.expect(ResourceConflictException.class);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java
index c3c2224..6c301da 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java
@@ -31,7 +31,7 @@
.that(actual.remove(g)).isTrue();
}
assert_().withFailureMessage("unexpected groups: " + actual)
- .that((Iterable<?>)actual).isEmpty();
+ .that(actual).isEmpty();
}
public static void assertGroupInfo(AccountGroup group, GroupInfo info) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 545be3e..92a3382 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -21,18 +21,17 @@
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST;
-import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.HEAD;
import static org.junit.Assert.fail;
import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.DraftApi;
@@ -40,7 +39,6 @@
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
-import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.SubmitType;
@@ -51,23 +49,19 @@
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.extensions.common.MergeableInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
-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.ETagView;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.server.change.GetRevisionActions;
import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.group.SystemGroupBackend;
-import com.google.gerrit.server.project.Util;
import com.google.inject.Inject;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.revwalk.RevCommit;
-import org.junit.Before;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
@@ -87,13 +81,6 @@
@Inject
private GetRevisionActions getRevisionActions;
- private TestAccount admin2;
-
- @Before
- public void setUp() throws Exception {
- admin2 = accounts.admin2();
- }
-
@Test
public void reviewTriplet() throws Exception {
PushOneCommit.Result r = createChange();
@@ -143,70 +130,6 @@
.isEqualTo(ChangeStatus.MERGED);
}
- private void allowSubmitOnBehalfOf() throws Exception {
- ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
- Util.allow(cfg,
- Permission.SUBMIT_AS,
- SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID(),
- "refs/heads/*");
- saveProjectConfig(project, cfg);
- }
-
- @Test
- public void submitOnBehalfOf() throws Exception {
- allowSubmitOnBehalfOf();
- PushOneCommit.Result r = createChange();
- String changeId = project.get() + "~master~" + r.getChangeId();
- gApi.changes()
- .id(changeId)
- .current()
- .review(ReviewInput.approve());
- SubmitInput in = new SubmitInput();
- in.onBehalfOf = admin2.email;
- gApi.changes()
- .id(changeId)
- .current()
- .submit(in);
- assertThat(gApi.changes().id(changeId).get().status)
- .isEqualTo(ChangeStatus.MERGED);
- }
-
- @Test
- public void submitOnBehalfOfInvalidUser() throws Exception {
- allowSubmitOnBehalfOf();
- PushOneCommit.Result r = createChange();
- String changeId = project.get() + "~master~" + r.getChangeId();
- gApi.changes()
- .id(changeId)
- .current()
- .review(ReviewInput.approve());
- SubmitInput in = new SubmitInput();
- in.onBehalfOf = "doesnotexist";
- exception.expect(UnprocessableEntityException.class);
- exception.expectMessage("Account Not Found: doesnotexist");
- gApi.changes()
- .id(changeId)
- .current()
- .submit(in);
- }
-
- @Test
- public void submitOnBehalfOfNotPermitted() throws Exception {
- PushOneCommit.Result r = createChange();
- gApi.changes()
- .id(project.get() + "~master~" + r.getChangeId())
- .current()
- .review(ReviewInput.approve());
- SubmitInput in = new SubmitInput();
- in.onBehalfOf = admin2.email;
- exception.expect(AuthException.class);
- exception.expectMessage("submit on behalf of not permitted");
- gApi.changes()
- .id(project.get() + "~master~" + r.getChangeId())
- .current()
- .submit(in);
- }
-
@Test
public void deleteDraft() throws Exception {
PushOneCommit.Result r = createDraft();
@@ -438,6 +361,111 @@
}
@Test
+ public void cherryPickMergeRelativeToDefaultParent() throws Exception {
+ String parent1FileName = "a.txt";
+ String parent2FileName = "b.txt";
+ PushOneCommit.Result mergeChangeResult =
+ createCherryPickableMerge(parent1FileName, parent2FileName);
+
+ String cherryPickBranchName = "branch_for_cherry_pick";
+ createBranch(new Branch.NameKey(project, cherryPickBranchName));
+
+ CherryPickInput cherryPickInput = new CherryPickInput();
+ cherryPickInput.destination = cherryPickBranchName;
+ cherryPickInput.message = "Cherry-pick a merge commit to another branch";
+
+ ChangeInfo cherryPickedChangeInfo = gApi.changes()
+ .id(mergeChangeResult.getChangeId())
+ .current()
+ .cherryPick(cherryPickInput)
+ .get();
+
+ Map<String, FileInfo> cherryPickedFilesByName =
+ cherryPickedChangeInfo.revisions
+ .get(cherryPickedChangeInfo.currentRevision)
+ .files;
+ assertThat(cherryPickedFilesByName).containsKey(parent2FileName);
+ assertThat(cherryPickedFilesByName).doesNotContainKey(parent1FileName);
+ }
+
+ @Test
+ public void cherryPickMergeRelativeToSpecificParent() throws Exception {
+ String parent1FileName = "a.txt";
+ String parent2FileName = "b.txt";
+ PushOneCommit.Result mergeChangeResult =
+ createCherryPickableMerge(parent1FileName, parent2FileName);
+
+ String cherryPickBranchName = "branch_for_cherry_pick";
+ createBranch(new Branch.NameKey(project, cherryPickBranchName));
+
+ CherryPickInput cherryPickInput = new CherryPickInput();
+ cherryPickInput.destination = cherryPickBranchName;
+ cherryPickInput.message = "Cherry-pick a merge commit to another branch";
+ cherryPickInput.parent = 2;
+
+ ChangeInfo cherryPickedChangeInfo = gApi.changes()
+ .id(mergeChangeResult.getChangeId())
+ .current()
+ .cherryPick(cherryPickInput)
+ .get();
+
+ Map<String, FileInfo> cherryPickedFilesByName =
+ cherryPickedChangeInfo.revisions
+ .get(cherryPickedChangeInfo.currentRevision)
+ .files;
+ assertThat(cherryPickedFilesByName).containsKey(parent1FileName);
+ assertThat(cherryPickedFilesByName).doesNotContainKey(parent2FileName);
+ }
+
+ @Test
+ public void cherryPickMergeUsingInvalidParent() throws Exception {
+ String parent1FileName = "a.txt";
+ String parent2FileName = "b.txt";
+ PushOneCommit.Result mergeChangeResult =
+ createCherryPickableMerge(parent1FileName, parent2FileName);
+
+ String cherryPickBranchName = "branch_for_cherry_pick";
+ createBranch(new Branch.NameKey(project, cherryPickBranchName));
+
+ CherryPickInput cherryPickInput = new CherryPickInput();
+ cherryPickInput.destination = cherryPickBranchName;
+ cherryPickInput.message = "Cherry-pick a merge commit to another branch";
+ cherryPickInput.parent = 0;
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("Cherry Pick: Parent 0 does not exist. Please"
+ + " specify a parent in range [1, 2].");
+ gApi.changes()
+ .id(mergeChangeResult.getChangeId())
+ .current()
+ .cherryPick(cherryPickInput);
+ }
+
+ @Test
+ public void cherryPickMergeUsingNonExistentParent() throws Exception {
+ String parent1FileName = "a.txt";
+ String parent2FileName = "b.txt";
+ PushOneCommit.Result mergeChangeResult =
+ createCherryPickableMerge(parent1FileName, parent2FileName);
+
+ String cherryPickBranchName = "branch_for_cherry_pick";
+ createBranch(new Branch.NameKey(project, cherryPickBranchName));
+
+ CherryPickInput cherryPickInput = new CherryPickInput();
+ cherryPickInput.destination = cherryPickBranchName;
+ cherryPickInput.message = "Cherry-pick a merge commit to another branch";
+ cherryPickInput.parent = 3;
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("Cherry Pick: Parent 3 does not exist. Please"
+ + " specify a parent in range [1, 2].");
+ gApi.changes()
+ .id(mergeChangeResult.getChangeId())
+ .current()
+ .cherryPick(cherryPickInput);
+ }
+
+ @Test
public void canRebase() throws Exception {
PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
PushOneCommit.Result r1 = push.to("refs/for/master");
@@ -887,4 +915,35 @@
assertDiffForNewFile(diff, pushResult.getCommit(), path,
expectedContentSideB);
}
+
+ private PushOneCommit.Result createCherryPickableMerge(String parent1FileName,
+ String parent2FileName) throws Exception {
+ RevCommit initialCommit = getHead(repo());
+
+ String branchAName = "branchA";
+ createBranch(new Branch.NameKey(project, branchAName));
+ String branchBName = "branchB";
+ createBranch(new Branch.NameKey(project, branchBName));
+
+ PushOneCommit.Result changeAResult = pushFactory
+ .create(db, admin.getIdent(), testRepo, "change a",
+ parent1FileName, "Content of a")
+ .to("refs/for/" + branchAName);
+
+ testRepo.reset(initialCommit);
+ PushOneCommit.Result changeBResult = pushFactory
+ .create(db, admin.getIdent(), testRepo, "change b",
+ parent2FileName, "Content of b")
+ .to("refs/for/" + branchBName);
+
+ PushOneCommit pushableMergeCommit = pushFactory.create(db, admin.getIdent(),
+ testRepo, "merge", ImmutableMap.of(parent1FileName, "Content of a",
+ parent2FileName, "Content of b"));
+ pushableMergeCommit.setParents(ImmutableList.of(changeAResult.getCommit(),
+ changeBResult.getCommit()));
+ PushOneCommit.Result mergeChangeResult =
+ pushableMergeCommit.to("refs/for/" + branchAName);
+ mergeChangeResult.assertOkStatus();
+ return mergeChangeResult;
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index 064b206..7aa5876 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -76,6 +76,31 @@
}
@Test
+ public void noOptionalFields() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ PushOneCommit.Result r = createChange();
+ RobotCommentInput in = createRobotCommentInputWithMandatoryFields();
+ ReviewInput reviewInput = new ReviewInput();
+ Map<String, List<RobotCommentInput>> robotComments = new HashMap<>();
+ robotComments.put(in.path, Collections.singletonList(in));
+ reviewInput.robotComments = robotComments;
+ reviewInput.message = "comment test";
+ gApi.changes()
+ .id(r.getChangeId())
+ .current()
+ .review(reviewInput);
+
+ Map<String, List<RobotCommentInfo>> out = gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .robotComments();
+ assertThat(out).hasSize(1);
+ RobotCommentInfo comment = Iterables.getOnlyElement(out.get(in.path));
+ assertRobotComment(comment, in, false);
+ }
+
+ @Test
public void robotCommentsNotSupported() throws Exception {
assume().that(notesMigration.enabled()).isFalse();
@@ -95,17 +120,25 @@
.review(reviewInput);
}
- private RobotCommentInput createRobotCommentInput() {
+ private RobotCommentInput createRobotCommentInputWithMandatoryFields() {
RobotCommentInput in = new RobotCommentInput();
in.robotId = "happyRobot";
in.robotRunId = "1";
- in.url = "http://www.happy-robot.com";
in.line = 1;
in.message = "nit: trailing whitespace";
in.path = FILE_NAME;
return in;
}
+ private RobotCommentInput createRobotCommentInput() {
+ RobotCommentInput in = createRobotCommentInputWithMandatoryFields();
+ in.url = "http://www.happy-robot.com";
+ in.properties = new HashMap<>();
+ in.properties.put("key1", "value1");
+ in.properties.put("key2", "value2");
+ return in;
+ }
+
private void assertRobotComment(RobotCommentInfo c,
RobotCommentInput expected) {
assertRobotComment(c, expected, true);
@@ -116,6 +149,7 @@
assertThat(c.robotId).isEqualTo(expected.robotId);
assertThat(c.robotRunId).isEqualTo(expected.robotRunId);
assertThat(c.url).isEqualTo(expected.url);
+ assertThat(c.properties).isEqualTo(expected.properties);
assertThat(c.line).isEqualTo(expected.line);
assertThat(c.message).isEqualTo(expected.message);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
new file mode 100644
index 0000000..c1f4237
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -0,0 +1,656 @@
+// 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.acceptance.rest.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.extensions.api.changes.DraftInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
+import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
+import com.google.gerrit.extensions.api.changes.RevisionApi;
+import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.client.Side;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.project.Util;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+
+import org.apache.http.Header;
+import org.apache.http.message.BasicHeader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ImpersonationIT extends AbstractDaemonTest {
+ @Inject
+ private AccountControl.Factory accountControlFactory;
+
+ @Inject
+ private ApprovalsUtil approvalsUtil;
+
+ @Inject
+ private ChangeMessagesUtil cmUtil;
+
+ @Inject
+ private CommentsUtil commentsUtil;
+
+ private RestSession anonRestSession;
+ private TestAccount admin2;
+ private GroupInfo newGroup;
+
+ @Before
+ public void setUp() throws Exception {
+ anonRestSession = new RestSession(server, null);
+ admin2 = accounts.admin2();
+ GroupInput gi = new GroupInput();
+ gi.name = name("New-Group");
+ gi.members = ImmutableList.of(user.id.toString());
+ newGroup = gApi.groups().create(gi).get();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ removeRunAs();
+ }
+
+ @Test
+ public void voteOnBehalfOf() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = user.id.toString();
+ in.message = "Message on behalf of";
+ revision.review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(
+ r.getChange().approvals().values());
+ assertThat(psa.getPatchSetId().get()).isEqualTo(1);
+ assertThat(psa.getLabel()).isEqualTo("Code-Review");
+ assertThat(psa.getAccountId()).isEqualTo(user.id);
+ assertThat(psa.getValue()).isEqualTo(1);
+ assertThat(psa.getRealAccountId()).isEqualTo(admin.id);
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(db, cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(user.id);
+ assertThat(m.getRealAuthor()).isEqualTo(admin.id);
+ }
+
+ @Test
+ public void voteOnBehalfOfRequiresLabel() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.message = "Message on behalf of";
+
+ exception.expect(AuthException.class);
+ exception.expectMessage(
+ "label required to post review on behalf of \"" + in.onBehalfOf + '"');
+ revision.review(in);
+ }
+
+ @Test
+ public void voteOnBehalfOfInvalidLabel() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.strictLabels = true;
+ in.label("Not-A-Label", 5);
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage(
+ "label \"Not-A-Label\" is not a configured label");
+ revision.review(in);
+ }
+
+ @Test
+ public void voteOnBehalfOfInvalidLabelIgnoredWithoutStrictLabels()
+ throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.strictLabels = false;
+ in.label("Code-Review", 1);
+ in.label("Not-A-Label", 5);
+
+ revision.review(in);
+
+ assertThat(gApi.changes().id(r.getChangeId()).get().labels)
+ .doesNotContainKey("Not-A-Label");
+ }
+
+ @Test
+ public void voteOnBehalfOfLabelNotPermitted() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ LabelType verified = Util.verified();
+ cfg.getLabelSections().put(verified.getName(), verified);
+ saveProjectConfig(project, cfg);
+
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.label("Verified", 1);
+
+ exception.expect(AuthException.class);
+ exception.expectMessage(
+ "not permitted to modify label \"Verified\" on behalf of \""
+ + in.onBehalfOf + '"');
+ revision.review(in);
+ }
+
+ @Test
+ public void voteOnBehalfOfWithComment() throws Exception {
+ testVoteOnBehalfOfWithComment();
+ }
+
+ @GerritConfig(name = "notedb.writeJson", value = "true")
+ @Test
+ public void voteOnBehalfOfWithCommentWritingJson() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ testVoteOnBehalfOfWithComment();
+ }
+
+ private void testVoteOnBehalfOfWithComment() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.label("Code-Review", 1);
+ CommentInput ci = new CommentInput();
+ ci.path = Patch.COMMIT_MSG;
+ ci.side = Side.REVISION;
+ ci.line = 1;
+ ci.message = "message";
+ in.comments = ImmutableMap.of(ci.path, ImmutableList.of(ci));
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ PatchSetApproval psa = Iterables.getOnlyElement(
+ r.getChange().approvals().values());
+ assertThat(psa.getPatchSetId().get()).isEqualTo(1);
+ assertThat(psa.getLabel()).isEqualTo("Code-Review");
+ assertThat(psa.getAccountId()).isEqualTo(user.id);
+ assertThat(psa.getValue()).isEqualTo(1);
+ assertThat(psa.getRealAccountId()).isEqualTo(admin.id);
+
+ ChangeData cd = r.getChange();
+ Comment c = Iterables.getOnlyElement(
+ commentsUtil.publishedByChange(db, cd.notes()));
+ assertThat(c.message).isEqualTo(ci.message);
+ assertThat(c.author.getId()).isEqualTo(user.id);
+ assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id);
+ }
+
+ @GerritConfig(name = "notedb.writeJson", value = "true")
+ @Test
+ public void voteOnBehalfOfWithRobotComment() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.label("Code-Review", 1);
+ RobotCommentInput ci = new RobotCommentInput();
+ ci.robotId = "my-robot";
+ ci.robotRunId = "abcd1234";
+ ci.path = Patch.COMMIT_MSG;
+ ci.side = Side.REVISION;
+ ci.line = 1;
+ ci.message = "message";
+ in.robotComments = ImmutableMap.of(ci.path, ImmutableList.of(ci));
+ gApi.changes().id(r.getChangeId()).current().review(in);
+
+ ChangeData cd = r.getChange();
+ RobotComment c = Iterables.getOnlyElement(
+ commentsUtil.robotCommentsByChange(cd.notes()));
+ assertThat(c.message).isEqualTo(ci.message);
+ assertThat(c.robotId).isEqualTo(ci.robotId);
+ assertThat(c.robotRunId).isEqualTo(ci.robotRunId);
+ assertThat(c.author.getId()).isEqualTo(user.id);
+ assertThat(c.getRealAuthor().getId()).isEqualTo(admin.id);
+ }
+
+ @Test
+ public void voteOnBehalfOfCannotModifyDrafts() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+
+ setApiUser(user);
+ DraftInput di = new DraftInput();
+ di.path = Patch.COMMIT_MSG;
+ di.side = Side.REVISION;
+ di.line = 1;
+ di.message = "message";
+ gApi.changes().id(r.getChangeId()).current().createDraft(di);
+
+ setApiUser(admin);
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.label("Code-Review", 1);
+ in.drafts = DraftHandling.PUBLISH;
+
+ exception.expect(AuthException.class);
+ exception.expectMessage("not allowed to modify other user's drafts");
+ gApi.changes().id(r.getChangeId()).current().review(in);
+ }
+
+ @Test
+ public void voteOnBehalfOfMissingUser() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = "doesnotexist";
+ in.label("Code-Review", 1);
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("Account Not Found: doesnotexist");
+ revision.review(in);
+ }
+
+ @Test
+ public void voteOnBehalfOfFailsWhenUserCannotSeeDestinationRef()
+ throws Exception {
+ blockRead(newGroup);
+
+ allowCodeReviewOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.label("Code-Review", 1);
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage(
+ "on_behalf_of account " + user.id + " cannot see destination ref");
+ revision.review(in);
+ }
+
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ @Test
+ public void voteOnBehalfOfInvisibleUserNotAllowed() throws Exception {
+ allowCodeReviewOnBehalfOf();
+ setApiUser(accounts.user2());
+ assertThat(accountControlFactory.get().canSee(user.id)).isFalse();
+
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.label("Code-Review", 1);
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("Account Not Found: " + in.onBehalfOf);
+ revision.review(in);
+ }
+
+ @Test
+ public void submitOnBehalfOf() throws Exception {
+ allowSubmitOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ String changeId = project.get() + "~master~" + r.getChangeId();
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .review(ReviewInput.approve());
+ SubmitInput in = new SubmitInput();
+ in.onBehalfOf = admin2.email;
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .submit(in);
+
+ ChangeData cd = r.getChange();
+ assertThat(cd.change().getStatus()).isEqualTo(Change.Status.MERGED);
+ PatchSetApproval submitter = approvalsUtil.getSubmitter(
+ db, cd.notes(), cd.change().currentPatchSetId());
+ assertThat(submitter.getAccountId()).isEqualTo(admin2.id);
+ assertThat(submitter.getRealAccountId()).isEqualTo(admin.id);
+ }
+
+ @Test
+ public void submitOnBehalfOfInvalidUser() throws Exception {
+ allowSubmitOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ String changeId = project.get() + "~master~" + r.getChangeId();
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .review(ReviewInput.approve());
+ SubmitInput in = new SubmitInput();
+ in.onBehalfOf = "doesnotexist";
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("Account Not Found: doesnotexist");
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .submit(in);
+ }
+
+ @Test
+ public void submitOnBehalfOfNotPermitted() throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes()
+ .id(project.get() + "~master~" + r.getChangeId())
+ .current()
+ .review(ReviewInput.approve());
+ SubmitInput in = new SubmitInput();
+ in.onBehalfOf = admin2.email;
+ exception.expect(AuthException.class);
+ exception.expectMessage("submit on behalf of not permitted");
+ gApi.changes()
+ .id(project.get() + "~master~" + r.getChangeId())
+ .current()
+ .submit(in);
+ }
+
+ @Test
+ public void submitOnBehalfOfFailsWhenUserCannotSeeDestinationRef()
+ throws Exception {
+ blockRead(newGroup);
+
+ allowSubmitOnBehalfOf();
+ PushOneCommit.Result r = createChange();
+ String changeId = project.get() + "~master~" + r.getChangeId();
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .review(ReviewInput.approve());
+ SubmitInput in = new SubmitInput();
+ in.onBehalfOf = user.email;
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage(
+ "on_behalf_of account " + user.id + " cannot see destination ref");
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .submit(in);
+ }
+
+ @GerritConfig(name = "accounts.visibility", value = "SAME_GROUP")
+ @Test
+ public void submitOnBehalfOfInvisibleUserNotAllowed() throws Exception {
+ allowSubmitOnBehalfOf();
+ setApiUser(accounts.user2());
+ assertThat(accountControlFactory.get().canSee(user.id)).isFalse();
+
+ PushOneCommit.Result r = createChange();
+ String changeId = project.get() + "~master~" + r.getChangeId();
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .review(ReviewInput.approve());
+ SubmitInput in = new SubmitInput();
+ in.onBehalfOf = user.email;
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("Account Not Found: " + in.onBehalfOf);
+ gApi.changes()
+ .id(changeId)
+ .current()
+ .submit(in);
+ }
+
+ @Test
+ public void runAsValidUser() throws Exception {
+ allowRunAs();
+ RestResponse res =
+ adminRestSession.getWithHeader("/accounts/self", runAsHeader(user.id));
+ res.assertOK();
+ AccountInfo account =
+ newGson().fromJson(res.getEntityContent(), AccountInfo.class);
+ assertThat(account._accountId).isEqualTo(user.id.get());
+ }
+
+ @GerritConfig(name = "auth.enableRunAs", value = "false")
+ @Test
+ public void runAsDisabledByConfig() throws Exception {
+ allowRunAs();
+ RestResponse res =
+ adminRestSession.getWithHeader("/changes/", runAsHeader(user.id));
+ res.assertForbidden();
+ assertThat(res.getEntityContent())
+ .isEqualTo("X-Gerrit-RunAs disabled by auth.enableRunAs = false");
+ }
+
+ @Test
+ public void runAsNotPermitted() throws Exception {
+ RestResponse res =
+ adminRestSession.getWithHeader("/changes/", runAsHeader(user.id));
+ res.assertForbidden();
+ assertThat(res.getEntityContent())
+ .isEqualTo("not permitted to use X-Gerrit-RunAs");
+ }
+
+ @Test
+ public void runAsNeverPermittedForAnonymousUsers() throws Exception {
+ allowRunAs();
+ RestResponse res =
+ anonRestSession.getWithHeader("/changes/", runAsHeader(user.id));
+ res.assertForbidden();
+ assertThat(res.getEntityContent())
+ .isEqualTo("not permitted to use X-Gerrit-RunAs");
+ }
+
+ @Test
+ public void runAsInvalidUser() throws Exception {
+ allowRunAs();
+ RestResponse res = adminRestSession.getWithHeader(
+ "/changes/", runAsHeader("doesnotexist"));
+ res.assertForbidden();
+ assertThat(res.getEntityContent())
+ .isEqualTo("no account matches X-Gerrit-RunAs");
+ }
+
+ @Test
+ public void voteUsingRunAsAvoidsRestrictionsOfOnBehalfOf() throws Exception {
+ allowRunAs();
+ PushOneCommit.Result r = createChange();
+
+ setApiUser(user);
+ DraftInput di = new DraftInput();
+ di.path = Patch.COMMIT_MSG;
+ di.side = Side.REVISION;
+ di.line = 1;
+ di.message = "inline comment";
+ gApi.changes().id(r.getChangeId()).current().createDraft(di);
+ setApiUser(admin);
+
+ // Things that aren't allowed with on_behalf_of:
+ // - no labels.
+ // - publish other user's drafts.
+ ReviewInput in = new ReviewInput();
+ in.message = "message";
+ in.drafts = DraftHandling.PUBLISH;
+ RestResponse res = adminRestSession.postWithHeader(
+ "/changes/" + r.getChangeId() + "/revisions/current/review", in,
+ runAsHeader(user.id));
+ res.assertOK();
+
+ ChangeMessageInfo m = Iterables.getLast(
+ gApi.changes().id(r.getChangeId()).get().messages);
+ assertThat(m.message).endsWith(in.message);
+ assertThat(m.author._accountId).isEqualTo(user.id.get());
+
+ CommentInfo c = Iterables.getOnlyElement(
+ gApi.changes().id(r.getChangeId()).comments().get(di.path));
+ assertThat(c.author._accountId).isEqualTo(user.id.get());
+ assertThat(c.message).isEqualTo(di.message);
+
+ setApiUser(user);
+ assertThat(gApi.changes().id(r.getChangeId()).drafts()).isEmpty();
+ }
+
+ @Test
+ public void runAsWithOnBehalfOf() throws Exception {
+ // - Has the same restrictions as on_behalf_of (e.g. requires labels).
+ // - Takes the effective user from on_behalf_of (user).
+ // - Takes the real user from the real caller, not the intermediate
+ // X-Gerrit-RunAs user (user2).
+ allowRunAs();
+ allowCodeReviewOnBehalfOf();
+ TestAccount user2 = accounts.user2();
+
+ PushOneCommit.Result r = createChange();
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.message = "Message on behalf of";
+
+ String endpoint =
+ "/changes/" + r.getChangeId() + "/revisions/current/review";
+ RestResponse res =
+ adminRestSession.postWithHeader(endpoint, in, runAsHeader(user2.id));
+ res.assertForbidden();
+ assertThat(res.getEntityContent()).isEqualTo(
+ "label required to post review on behalf of \"" + in.onBehalfOf + '"');
+
+ in.label("Code-Review", 1);
+ adminRestSession.postWithHeader(endpoint, in, runAsHeader(user2.id))
+ .assertOK();
+
+ PatchSetApproval psa = Iterables.getOnlyElement(
+ r.getChange().approvals().values());
+ assertThat(psa.getPatchSetId().get()).isEqualTo(1);
+ assertThat(psa.getLabel()).isEqualTo("Code-Review");
+ assertThat(psa.getAccountId()).isEqualTo(user.id);
+ assertThat(psa.getValue()).isEqualTo(1);
+ assertThat(psa.getRealAccountId()).isEqualTo(admin.id); // not user2
+
+ ChangeData cd = r.getChange();
+ ChangeMessage m = Iterables.getLast(cmUtil.byChange(db, cd.notes()));
+ assertThat(m.getMessage()).endsWith(in.message);
+ assertThat(m.getAuthor()).isEqualTo(user.id);
+ assertThat(m.getRealAuthor()).isEqualTo(admin.id); // not user2
+ }
+
+ private void allowCodeReviewOnBehalfOf() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ LabelType codeReviewType = Util.codeReview();
+ String forCodeReviewAs = Permission.forLabelAs(codeReviewType.getName());
+ String heads = "refs/heads/*";
+ AccountGroup.UUID uuid =
+ SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+ Util.allow(cfg, forCodeReviewAs, -1, 1, uuid, heads);
+ saveProjectConfig(project, cfg);
+ }
+
+ private void allowSubmitOnBehalfOf() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ String heads = "refs/heads/*";
+ AccountGroup.UUID uuid =
+ SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID();
+ Util.allow(cfg, Permission.SUBMIT_AS, uuid, heads);
+ Util.allow(cfg, Permission.SUBMIT, uuid, heads);
+ LabelType codeReviewType = Util.codeReview();
+ Util.allow(cfg, Permission.forLabel(codeReviewType.getName()),
+ -2, 2, uuid, heads);
+ saveProjectConfig(project, cfg);
+ }
+
+ private void blockRead(GroupInfo group) throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ Util.block(
+ cfg, Permission.READ, new AccountGroup.UUID(group.id), "refs/heads/master");
+ saveProjectConfig(project, cfg);
+ }
+
+ private void allowRunAs() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+ Util.allow(cfg, GlobalCapability.RUN_AS,
+ SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID());
+ saveProjectConfig(allProjects, cfg);
+ }
+
+ private void removeRunAs() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+ Util.remove(cfg, GlobalCapability.RUN_AS,
+ SystemGroupBackend.getGroup(ANONYMOUS_USERS).getUUID());
+ saveProjectConfig(allProjects, cfg);
+ }
+
+ private static Header runAsHeader(Object user) {
+ return new BasicHeader("X-Gerrit-RunAs", user.toString());
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
index a044772..0c53658 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -245,10 +245,8 @@
assertMessage(r, "Hashtag added: MyHashtag");
}
- private IterableSubject<
- ? extends IterableSubject<?, String, Iterable<String>>,
- String, Iterable<String>>
- assertThatGet(PushOneCommit.Result r) throws Exception {
+ private IterableSubject assertThatGet(PushOneCommit.Result r)
+ throws Exception {
return assertThat(gApi.changes()
.id(r.getChange().getId().get())
.getHashtags());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
index 0f251f5..b80abbb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -20,15 +20,20 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.GerritConfigs;
+import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
+import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.group.CreateGroup;
import com.google.gerrit.server.group.GroupsCollection;
import com.google.inject.Inject;
@@ -38,7 +43,9 @@
import java.util.Arrays;
import java.util.List;
+import java.util.stream.Collectors;
+@Sandboxed
public class SuggestReviewersIT extends AbstractDaemonTest {
@Inject
private CreateGroup.Factory createGroupFactory;
@@ -89,15 +96,6 @@
}
@Test
- @GerritConfig(name = "suggest.from", value = "2")
- public void suggestReviewersNoResult3() throws Exception {
- String changeId = createChange().getChangeId();
- List<SuggestedReviewerInfo> reviewers =
- suggestReviewers(changeId, name("").substring(0, 1), 6);
- assertThat(reviewers).isEmpty();
- }
-
- @Test
public void suggestReviewersChange() throws Exception {
String changeId = createChange().getChangeId();
List<SuggestedReviewerInfo> reviewers =
@@ -204,7 +202,7 @@
assertThat(reviewers).hasSize(1);
reviewers = suggestReviewers(changeId, "example.com", 7);
- assertThat(reviewers).hasSize(6);
+ assertThat(reviewers).hasSize(5);
reviewers = suggestReviewers(changeId, user1.email, 2);
assertThat(reviewers).hasSize(1);
@@ -267,6 +265,145 @@
assertThat(reviewer.confirm).isTrue();
}
+ @Test
+ public void defaultReviewerSuggestion() throws Exception{
+ TestAccount user1 = user("customuser1", "User1");
+ TestAccount reviewer1 = user("customuser2", "User2");
+ TestAccount reviewer2 = user("customuser3", "User3");
+
+ setApiUser(user1);
+ String changeId1 = createChangeFromApi();
+
+ setApiUser(reviewer1);
+ reviewChange(changeId1);
+
+ setApiUser(user1);
+ String changeId2 = createChangeFromApi();
+
+ setApiUser(reviewer1);
+ reviewChange(changeId2);
+
+ setApiUser(reviewer2);
+ reviewChange(changeId2);
+
+ setApiUser(user1);
+ List<SuggestedReviewerInfo> reviewers =
+ suggestReviewers(createChangeFromApi(), null, 4);
+ assertThat(
+ reviewers.stream()
+ .map(r -> r.account._accountId)
+ .collect(Collectors.toList()))
+ .containsExactly(
+ reviewer1.id.get(),
+ reviewer2.id.get())
+ .inOrder();
+ }
+
+ @Test
+ public void defaultReviewerSuggestionOnFirstChange() throws Exception{
+ TestAccount user1 = user("customuser1", "User1");
+ setApiUser(user1);
+ List<SuggestedReviewerInfo> reviewers =
+ suggestReviewers(createChange().getChangeId(), "", 4);
+ assertThat(reviewers).isEmpty();
+ }
+
+ @Test
+ @GerritConfig(name = "suggest.maxSuggestedReviewers", value = "10")
+ public void reviewerRanking() throws Exception{
+ // Assert that user are ranked by the number of times they have applied a
+ // a label to a change (highest), added comments (medium) or owned a
+ // change (low).
+ String fullName = "Primum Finalis";
+ TestAccount userWhoOwns = user("customuser1", fullName);
+ TestAccount reviewer1 = user("customuser2", fullName);
+ TestAccount reviewer2 = user("customuser3", fullName);
+ TestAccount userWhoComments = user("customuser4", fullName);
+ TestAccount userWhoLooksForSuggestions = user("customuser5", fullName);
+
+ // Create a change as userWhoOwns and add some reviews
+ setApiUser(userWhoOwns);
+ String changeId1 = createChangeFromApi();
+
+ setApiUser(reviewer1);
+ reviewChange(changeId1);
+
+ setApiUser(user1);
+ String changeId2 = createChangeFromApi();
+
+ setApiUser(reviewer1);
+ reviewChange(changeId2);
+
+ setApiUser(reviewer2);
+ reviewChange(changeId2);
+
+ // Create a comment as a different user
+ setApiUser(userWhoComments);
+ ReviewInput ri = new ReviewInput();
+ ri.message = "Test";
+ gApi.changes().id(changeId1).revision(1).review(ri);
+
+ // Create a change as a new user to assert that we receive the correct
+ // ranking
+
+ setApiUser(userWhoLooksForSuggestions);
+ List<SuggestedReviewerInfo> reviewers =
+ suggestReviewers(createChangeFromApi(), "Pri", 4);
+ assertThat(
+ reviewers.stream()
+ .map(r -> r.account._accountId)
+ .collect(Collectors.toList()))
+ .containsExactly(
+ reviewer1.id.get(),
+ reviewer2.id.get(),
+ userWhoOwns.id.get(),
+ userWhoComments.id.get())
+ .inOrder();
+ }
+
+ @Test
+ public void reviewerRankingProjectIsolation() throws Exception{
+ // Create new project
+ Project.NameKey newProject = createProject("test");
+
+ // Create users who review changes in both the default and the new project
+ String fullName = "Primum Finalis";
+ TestAccount userWhoOwns = user("customuser1", fullName);
+ TestAccount reviewer1 = user("customuser2", fullName);
+ TestAccount reviewer2 = user("customuser3", fullName);
+
+ setApiUser(userWhoOwns);
+ String changeId1 = createChangeFromApi();
+
+ setApiUser(reviewer1);
+ reviewChange(changeId1);
+
+ setApiUser(userWhoOwns);
+ String changeId2 = createChangeFromApi(newProject);
+
+ setApiUser(reviewer2);
+ reviewChange(changeId2);
+
+ setApiUser(userWhoOwns);
+ String changeId3 = createChangeFromApi(newProject);
+
+ setApiUser(reviewer2);
+ reviewChange(changeId3);
+
+ setApiUser(userWhoOwns);
+ List<SuggestedReviewerInfo> reviewers =
+ suggestReviewers(createChangeFromApi(), "Prim", 4);
+
+ // Assert that reviewer1 is on top, even though reviewer2 has more reviews
+ // in other projects
+ assertThat(
+ reviewers.stream()
+ .map(r -> r.account._accountId)
+ .collect(Collectors.toList()))
+ .containsExactly(reviewer1.id.get(), reviewer2.id.get())
+ .inOrder();
+ }
+
private List<SuggestedReviewerInfo> suggestReviewers(String changeId,
String query, int n) throws Exception {
return gApi.changes()
@@ -296,4 +433,23 @@
throws Exception {
return user(name, fullName, name, groups);
}
+
+ private void reviewChange(String changeId) throws RestApiException {
+ ReviewInput ri = new ReviewInput();
+ ri.label("Code-Review", 1);
+ gApi.changes().id(changeId).current().review(ri);
+ }
+
+ private String createChangeFromApi() throws RestApiException{
+ return createChangeFromApi(project);
+ }
+
+ private String createChangeFromApi(Project.NameKey project)
+ throws RestApiException{
+ ChangeInput ci = new ChangeInput();
+ ci.project = project.get();
+ ci.subject = "Test change at" + System.nanoTime();
+ ci.branch = "master";
+ return gApi.changes().create(ci).get().changeId;
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
index e081ce5..e3104bb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
@@ -31,12 +31,8 @@
import java.util.Set;
public class ProjectAssert {
- public static IterableSubject<
- ? extends IterableSubject<
- ?, Project.NameKey, Iterable<Project.NameKey>>,
- Project.NameKey,
- Iterable<Project.NameKey>>
- assertThatNameList(Iterable<ProjectInfo> actualIt) {
+ public static IterableSubject assertThatNameList(
+ Iterable<ProjectInfo> actualIt) {
List<ProjectInfo> actual = ImmutableList.copyOf(actualIt);
for (ProjectInfo info : actual) {
assertWithMessage("missing project name").that(info.name).isNotNull();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 77cb11b..34301ed 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -397,7 +397,7 @@
setApiUser(admin);
Map<String, List<CommentInfo>> actual =
gApi.changes().id(r1.getChangeId()).drafts();
- assertThat((Iterable<?>) actual.keySet()).containsExactly(FILE_NAME);
+ assertThat(actual.keySet()).containsExactly(FILE_NAME);
List<CommentInfo> comments = actual.get(FILE_NAME);
assertThat(comments).hasSize(2);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index a30674f..dcdce92 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.junit.Assert.fail;
@@ -25,14 +26,18 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Ordering;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
+import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Account;
@@ -41,6 +46,7 @@
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
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.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -54,8 +60,10 @@
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.git.RepoRefCache;
import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.notedb.ChangeBundle;
import com.google.gerrit.server.notedb.ChangeBundleReader;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -63,6 +71,7 @@
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
+import com.google.gerrit.server.project.Util;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.NoteDbChecker;
import com.google.gerrit.testutil.NoteDbMode;
@@ -72,6 +81,8 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
+import org.apache.http.Header;
+import org.apache.http.message.BasicHeader;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
@@ -85,6 +96,7 @@
import java.sql.Timestamp;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@@ -971,6 +983,94 @@
checker.rebuildAndCheckChanges(id);
}
+ @Test
+ public void rebuildEntitiesCreatedByImpersonation() throws Exception {
+ PushOneCommit.Result r = createChange();
+ Change.Id id = r.getPatchSetId().getParentKey();
+ PatchSet.Id psId = new PatchSet.Id(id, 1);
+ String prefix = "/changes/" + id + "/revisions/current/";
+
+ // For each of the entities that have a real user field, create one entity
+ // without impersonation and one with.
+ CommentInput ci = new CommentInput();
+ ci.path = Patch.COMMIT_MSG;
+ ci.side = Side.REVISION;
+ ci.line = 1;
+ ci.message = "comment without impersonation";
+ ReviewInput ri = new ReviewInput();
+ ri.label("Code-Review", -1);
+ ri.message = "message without impersonation";
+ ri.drafts = DraftHandling.KEEP;
+ ri.comments = ImmutableMap.of(ci.path, ImmutableList.of(ci));
+ userRestSession.post(prefix + "review", ri).assertOK();
+
+ DraftInput di = new DraftInput();
+ di.path = Patch.COMMIT_MSG;
+ di.side = Side.REVISION;
+ di.line = 1;
+ di.message = "draft without impersonation";
+ userRestSession.put(prefix + "drafts", di).assertCreated();
+
+ allowRunAs();
+ try {
+ Header runAs = new BasicHeader("X-Gerrit-RunAs", user.id.toString());
+ ci.message = "comment with impersonation";
+ ri.message = "message with impersonation";
+ ri.label("Code-Review", 1);
+ adminRestSession.postWithHeader(prefix + "review", ri, runAs).assertOK();
+
+ di.message = "draft with impersonation";
+ adminRestSession.putWithHeader(prefix + "drafts", runAs, di)
+ .assertCreated();
+ } finally {
+ removeRunAs();
+ }
+
+ List<ChangeMessage> msgs =
+ Ordering.natural().onResultOf(ChangeMessage::getWrittenOn)
+ .sortedCopy(db.changeMessages().byChange(id));
+ assertThat(msgs).hasSize(3);
+ assertThat(msgs.get(1).getMessage())
+ .endsWith("message without impersonation");
+ assertThat(msgs.get(1).getAuthor()).isEqualTo(user.id);
+ assertThat(msgs.get(1).getRealAuthor()).isEqualTo(user.id);
+ assertThat(msgs.get(2).getMessage()).endsWith("message with impersonation");
+ assertThat(msgs.get(2).getAuthor()).isEqualTo(user.id);
+ assertThat(msgs.get(2).getRealAuthor()).isEqualTo(admin.id);
+
+ List<PatchSetApproval> psas = db.patchSetApprovals().byChange(id).toList();
+ assertThat(psas).hasSize(1);
+ assertThat(psas.get(0).getLabel()).isEqualTo("Code-Review");
+ assertThat(psas.get(0).getValue()).isEqualTo(1);
+ assertThat(psas.get(0).getAccountId()).isEqualTo(user.id);
+ assertThat(psas.get(0).getRealAccountId()).isEqualTo(admin.id);
+
+ Ordering<PatchLineComment> commentOrder =
+ Ordering.natural().onResultOf(PatchLineComment::getWrittenOn);
+ List<PatchLineComment> drafts = commentOrder.sortedCopy(
+ db.patchComments().draftByPatchSetAuthor(psId, user.id));
+ assertThat(drafts).hasSize(2);
+ assertThat(drafts.get(0).getMessage())
+ .isEqualTo("draft without impersonation");
+ assertThat(drafts.get(0).getAuthor()).isEqualTo(user.id);
+ assertThat(drafts.get(0).getRealAuthor()).isEqualTo(user.id);
+ assertThat(drafts.get(1).getMessage())
+ .isEqualTo("draft with impersonation");
+ assertThat(drafts.get(1).getAuthor()).isEqualTo(user.id);
+ assertThat(drafts.get(1).getRealAuthor()).isEqualTo(admin.id);
+
+ List<PatchLineComment> pub = commentOrder.sortedCopy(
+ db.patchComments().publishedByPatchSet(psId));
+ assertThat(pub).hasSize(2);
+ assertThat(pub.get(0).getMessage())
+ .isEqualTo("comment without impersonation");
+ assertThat(pub.get(0).getAuthor()).isEqualTo(user.id);
+ assertThat(pub.get(0).getRealAuthor()).isEqualTo(user.id);
+ assertThat(pub.get(1).getMessage()).isEqualTo("comment with impersonation");
+ assertThat(pub.get(1).getAuthor()).isEqualTo(user.id);
+ assertThat(pub.get(1).getRealAuthor()).isEqualTo(admin.id);
+ }
+
private void assertChangesReadOnly(RestApiException e) throws Exception {
Throwable cause = e.getCause();
assertThat(cause).isInstanceOf(UpdateException.class);
@@ -1086,4 +1186,19 @@
ReviewDb db = dbProvider.get();
return ReviewDbUtil.unwrapDb(db);
}
+
+ private void allowRunAs() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+ Util.allow(cfg, GlobalCapability.RUN_AS,
+ SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
+ saveProjectConfig(allProjects, cfg);
+ }
+
+ private void removeRunAs() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
+ Util.remove(cfg, GlobalCapability.RUN_AS,
+ SystemGroupBackend.getGroup(REGISTERED_USERS).getUUID());
+ saveProjectConfig(allProjects, cfg);
+ }
+
}
diff --git a/gerrit-acceptance-tests/tests.bzl b/gerrit-acceptance-tests/tests.bzl
index 62a99e3..160234f 100644
--- a/gerrit-acceptance-tests/tests.bzl
+++ b/gerrit-acceptance-tests/tests.bzl
@@ -11,7 +11,8 @@
flaky = 0,
deps = [],
labels = [],
- vm_args = ['-Xmx256m']):
+ vm_args = ['-Xmx256m'],
+ **kwargs):
junit_tests(
name = group,
srcs = srcs,
@@ -24,4 +25,5 @@
'slow',
],
jvm_flags = vm_args,
+ **kwargs
)
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
index 7ae7ef1..2e1bb13 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
@@ -17,4 +17,5 @@
public class CherryPickInput {
public String message;
public String destination;
+ public Integer parent;
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 8f1e283..472559b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -49,8 +49,11 @@
/**
* How to process draft comments already in the database that were not also
* described in this input request.
+ * <p>
+ * Defaults to DELETE, unless {@link #onBehalfOf} is set, in which case it
+ * defaults to KEEP and any other value is disallowed.
*/
- public DraftHandling drafts = DraftHandling.DELETE;
+ public DraftHandling drafts;
/** Who to send email notifications to after review is stored. */
public NotifyHandling notify = NotifyHandling.ALL;
@@ -99,6 +102,7 @@
public String robotId;
public String robotRunId;
public String url;
+ public Map<String, String> properties;
}
public ReviewInput message(String msg) {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
index a6b7593..9028a1d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/RobotCommentInfo.java
@@ -14,8 +14,11 @@
package com.google.gerrit.extensions.common;
+import java.util.Map;
+
public class RobotCommentInfo extends CommentInfo {
public String robotId;
public String robotRunId;
public String url;
+ public Map<String, String> properties;
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
index 28052ef..6eb11bc 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -53,6 +53,7 @@
* @param member type of entry in the set.
*/
public static <T> void setOf(Binder binder, Class<T> member) {
+ binder.disableCircularProxies();
setOf(binder, TypeLiteral.get(member));
}
@@ -71,6 +72,7 @@
@SuppressWarnings("unchecked")
Key<DynamicSet<T>> key = (Key<DynamicSet<T>>) Key.get(
Types.newParameterizedType(DynamicSet.class, member.getType()));
+ binder.disableCircularProxies();
binder.bind(key)
.toProvider(new DynamicSetProvider<>(member))
.in(Scopes.SINGLETON);
@@ -84,6 +86,7 @@
* @return a binder to continue configuring the new set member.
*/
public static <T> LinkedBindingBuilder<T> bind(Binder binder, Class<T> type) {
+ binder.disableCircularProxies();
return bind(binder, TypeLiteral.get(type));
}
@@ -95,6 +98,7 @@
* @return a binder to continue configuring the new set member.
*/
public static <T> LinkedBindingBuilder<T> bind(Binder binder, TypeLiteral<T> type) {
+ binder.disableCircularProxies();
return binder.bind(type).annotatedWith(UniqueAnnotations.create());
}
@@ -110,6 +114,7 @@
public static <T> LinkedBindingBuilder<T> bind(Binder binder,
Class<T> type,
Named name) {
+ binder.disableCircularProxies();
return bind(binder, TypeLiteral.get(type));
}
@@ -125,6 +130,7 @@
public static <T> LinkedBindingBuilder<T> bind(Binder binder,
TypeLiteral<T> type,
Named name) {
+ binder.disableCircularProxies();
return binder.bind(type).annotatedWith(name);
}
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
index cf5a445..525a837 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
@@ -43,7 +43,7 @@
}
@Override
- public final void requestSuggestions(final Request request, final Callback cb) {
+ public final void requestSuggestions(Request request, Callback cb) {
onRequestSuggestions(request, new Callback() {
@Override
public void onSuggestionsReady(final Request request,
@@ -88,27 +88,28 @@
ds = escape(ds);
}
- StringBuilder pattern = new StringBuilder();
- for (String qterm : splitQuery(qstr)) {
- qterm = escape(qterm);
- // We now surround qstr by <strong>. But the chosen approach is not too
- // smooth, if qstr is small (e.g.: "t") and this small qstr may occur in
- // escapes (e.g.: "Tim <email@example.org>"). Those escapes will
- // get <strong>-ed as well (e.g.: "<" -> "&<strong>l</strong>t;"). But
- // as repairing those mangled escapes is easier than not mangling them in
- // the first place, we repair them afterwards.
-
- if (pattern.length() > 0) {
- pattern.append("|");
+ if (qstr != null && !qstr.isEmpty()) {
+ StringBuilder pattern = new StringBuilder();
+ for (String qterm : splitQuery(qstr)) {
+ qterm = escape(qterm);
+ // We now surround qstr by <strong>. But the chosen approach is not too
+ // smooth, if qstr is small (e.g.: "t") and this small qstr may occur in
+ // escapes (e.g.: "Tim <email@example.org>"). Those escapes will
+ // get <strong>-ed as well (e.g.: "<" -> "&<strong>l</strong>t;"). But
+ // as repairing those mangled escapes is easier than not mangling them in
+ // the first place, we repair them afterwards.
+ if (pattern.length() > 0) {
+ pattern.append("|");
+ }
+ pattern.append(qterm);
}
- pattern.append(qterm);
+
+ ds = sgi(ds, "(" + pattern.toString() + ")", "<strong>$1</strong>");
+
+ // Repairing <strong>-ed escapes.
+ ds = sgi(ds, "(&[a-z]*)<strong>([a-z]*)</strong>([a-z]*;)", "$1$2$3");
}
- ds = sgi(ds, "(" + pattern.toString() + ")", "<strong>$1</strong>");
-
- // Repairing <strong>-ed escapes.
- ds = sgi(ds, "(&[a-z]*)<strong>([a-z]*)</strong>([a-z]*;)", "$1$2$3");
-
displayString = ds;
}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
index cf7e1d8..5a6918a 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/ui/RemoteSuggestOracle.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client.ui;
+import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.SuggestOracle;
/**
@@ -31,6 +32,10 @@
private final SuggestOracle oracle;
private Query query;
private String last;
+ private Timer requestRetentionTimer;
+ private boolean cancelOutstandingRequest;
+
+ private boolean serveSuggestions;
public RemoteSuggestOracle(SuggestOracle src) {
oracle = src;
@@ -42,13 +47,33 @@
@Override
public void requestSuggestions(Request req, Callback cb) {
- Query q = new Query(req, cb);
- if (query == null) {
- query = q;
- q.start();
- } else {
- query = q;
+ if (!serveSuggestions){
+ return;
}
+
+ // Use a timer for key stroke retention, such that we don't query the
+ // backend for each and every keystroke we receive.
+ if (requestRetentionTimer != null) {
+ requestRetentionTimer.cancel();
+ }
+ requestRetentionTimer = new Timer() {
+ @Override
+ public void run() {
+ Query q = new Query(req, cb);
+ if (query == null) {
+ query = q;
+ q.start();
+ } else {
+ query = q;
+ }
+ }
+ };
+ requestRetentionTimer.schedule(200);
+ }
+
+ @Override
+ public void requestDefaultSuggestions(Request req, Callback cb) {
+ requestSuggestions(req, cb);
}
@Override
@@ -56,6 +81,19 @@
return oracle.isDisplayStringHTML();
}
+ public void cancelOutstandingRequest() {
+ if (requestRetentionTimer != null) {
+ requestRetentionTimer.cancel();
+ }
+ if (query != null) {
+ cancelOutstandingRequest = true;
+ }
+ }
+
+ public void setServeSuggestions(boolean serveSuggestions) {
+ this.serveSuggestions = serveSuggestions;
+ }
+
private class Query implements Callback {
final Request request;
final Callback callback;
@@ -71,7 +109,11 @@
@Override
public void onSuggestionsReady(Request req, Response res) {
- if (query == this) {
+ if (cancelOutstandingRequest || !serveSuggestions) {
+ // If cancelOutstandingRequest() was called, we ignore this response
+ cancelOutstandingRequest = false;
+ query = null;
+ } else if (query == this) {
// No new request was started while this query was running.
// Propose this request's response as the suggestions.
query = null;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
index 7921ebe..2956ffc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
@@ -15,6 +15,7 @@
package com.google.gerrit.client.change;
import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.NotSignedInDialog;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.Util;
@@ -36,6 +37,8 @@
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.EventListener;
import com.google.gwt.user.client.rpc.StatusCodeException;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;
@@ -161,6 +164,10 @@
public void onSuccess(AccountInfo result) {
onCloseForm();
setAssignee(result);
+ Reviewers reviewers = getReviewers();
+ if (reviewers != null) {
+ reviewers.updateReviewerList();
+ }
}
@Override
@@ -180,7 +187,7 @@
private void setAssignee(AccountInfo assignee) {
currentAssignee = assignee;
- assigneeLink.setText(assignee != null ? assignee.name() : null);
+ assigneeLink.setText(assignee != null ? getName(assignee) : null);
assigneeLink.setTargetHistoryToken(assignee != null
? PageLinks.toAssigneeQuery(assignee.name() != null
? assignee.name()
@@ -189,4 +196,26 @@
: String.valueOf(assignee._accountId()))
: "");
}
+
+ private Reviewers getReviewers() {
+ Element e = DOM.getParent(getElement());
+ for (e = DOM.getParent(e); e != null; e = DOM.getParent(e)) {
+ EventListener l = DOM.getEventListener(e);
+ if (l instanceof ChangeScreen) {
+ ChangeScreen screen = (ChangeScreen) l;
+ return screen.reviewers;
+ }
+ }
+ return null;
+ }
+
+ private String getName(AccountInfo info) {
+ if (info.name() != null) {
+ return info.name();
+ }
+ if (info.email() != null) {
+ return info.email();
+ }
+ return Gerrit.info().user().anonymousCowardName();
+ }
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
index 404f3c8..6f518b1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
@@ -21,21 +21,21 @@
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.AccountSuggestOracle;
-import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** REST API based suggestion Oracle for reviewers. */
-public class ReviewerSuggestOracle extends SuggestAfterTypingNCharsOracle {
+public class ReviewerSuggestOracle extends HighlightSuggestOracle {
private Change.Id changeId;
@Override
- protected void _onRequestSuggestions(final Request req, final Callback cb) {
+ protected void onRequestSuggestions(final Request req, final Callback cb) {
ChangeApi
.suggestReviewers(changeId.get(), req.getQuery(), req.getLimit(), false)
.get(new GerritCallback<JsArray<SuggestReviewerInfo>>() {
@@ -56,6 +56,11 @@
});
}
+ @Override
+ public void requestDefaultSuggestions(final Request req, final Callback cb) {
+ requestSuggestions(req, cb);
+ }
+
public void setChange(Change.Id changeId) {
this.changeId = changeId;
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
index b69d1c0..e0c252c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -81,6 +81,7 @@
Reviewers() {
reviewerSuggestOracle = new ReviewerSuggestOracle();
suggestBox = new RemoteSuggestBox(reviewerSuggestOracle);
+ suggestBox.enableDefaultSuggestions();
suggestBox.setVisibleLength(55);
suggestBox.setHintText(Util.C.approvalTableAddReviewerHint());
suggestBox.addCloseHandler(new CloseHandler<RemoteSuggestBox>() {
@@ -123,6 +124,7 @@
UIObject.setVisible(form, true);
UIObject.setVisible(error, false);
addReviewerIcon.setVisible(false);
+ suggestBox.setServeSuggestionsOnOracle(true);
suggestBox.setFocus(true);
}
@@ -143,6 +145,7 @@
UIObject.setVisible(form, false);
suggestBox.setFocus(false);
suggestBox.setText("");
+ suggestBox.setServeSuggestionsOnOracle(false);
}
private void addReviewer(final String reviewer, boolean confirmed) {
@@ -198,7 +201,7 @@
});
}
- private void updateReviewerList() {
+ void updateReviewerList() {
ChangeApi.detail(changeId.get(),
new GerritCallback<ChangeInfo>() {
@Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index 4882b97..a008149 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -171,10 +171,13 @@
}
public static RestApi suggestReviewers(int id, String q, int n, boolean e) {
- return change(id).view("suggest_reviewers")
- .addParameter("q", q)
+ RestApi api = change(id).view("suggest_reviewers")
.addParameter("n", n)
.addParameter("e", e);
+ if (q != null) {
+ api.addParameter("q", q);
+ }
+ return api;
}
public static RestApi vote(int id, int reviewer, String vote) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
index 3702e68..bfeeaec 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
@@ -61,7 +61,8 @@
public static String format(AccountInfo info, String query) {
String s = FormatUtil.nameEmail(info);
- if (!containsQuery(s, query) && info.secondaryEmails() != null) {
+ if (query != null && !containsQuery(s, query) &&
+ info.secondaryEmails() != null) {
for (String email : Natives.asList(info.secondaryEmails())) {
AccountInfo info2 = AccountInfo.create(info._accountId(), info.name(),
email, info.username());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
index 084cb9a..57cd849 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/RemoteSuggestBox.java
@@ -13,6 +13,8 @@
// limitations under the License.
package com.google.gerrit.client.ui;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
@@ -42,6 +44,7 @@
public RemoteSuggestBox(SuggestOracle oracle) {
remoteSuggestOracle = new RemoteSuggestOracle(oracle);
+ remoteSuggestOracle.setServeSuggestions(true);
display = new DefaultSuggestionDisplay();
textBox = new HintTextBox();
@@ -49,7 +52,6 @@
@Override
public void onKeyDown(KeyDownEvent e) {
submitOnSelection = false;
-
if (e.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
CloseEvent.fire(RemoteSuggestBox.this, RemoteSuggestBox.this);
} else if (e.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
@@ -70,10 +72,11 @@
suggestBox.addSelectionHandler(new SelectionHandler<Suggestion>() {
@Override
public void onSelection(SelectionEvent<Suggestion> event) {
- textBox.setFocus(true);
if (submitOnSelection) {
SelectionEvent.fire(RemoteSuggestBox.this, getText());
}
+ remoteSuggestOracle.cancelOutstandingRequest();
+ display.hideSuggestions();
}
});
initWidget(suggestBox);
@@ -138,4 +141,19 @@
public void selectAll() {
suggestBox.getValueBox().selectAll();
}
+
+ public void enableDefaultSuggestions() {
+ textBox.addFocusHandler(new FocusHandler() {
+ @Override
+ public void onFocus(FocusEvent focusEvent) {
+ if (textBox.getText().equals("")) {
+ suggestBox.showSuggestionList();
+ }
+ }
+ });
+ }
+
+ public void setServeSuggestionsOnOracle(boolean serveSuggestions) {
+ remoteSuggestOracle.setServeSuggestions(serveSuggestions);
+ }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index 210800d..7e71639 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -90,7 +90,10 @@
}
CurrentUser self = session.get().getUser();
- if (!self.getCapabilities().canRunAs()) {
+ if (!self.getCapabilities().canRunAs()
+ // Always disallow for anonymous users, even if permitted by the ACL,
+ // because that would be crazy.
+ || !self.isIdentifiedUser()) {
replyError(req, res,
SC_FORBIDDEN,
"not permitted to use " + RUN_AS,
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 8d8937c..18445b3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -98,6 +98,11 @@
serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
filter("/a/*").through(RequireIdentifiedUserFilter.class);
+
+ // Must be after RequireIdentifiedUserFilter so auth happens before checking
+ // for RunAs capability.
+ install(new RunAsFilter.Module());
+
serveRegex("^/(?:a/)?tools/(.*)$").with(ToolServlet.class);
// Bind servlets for REST root collections.
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index fa551e8..d5a4b00 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -56,8 +56,6 @@
bind(RequestScopePropagator.class).to(GuiceRequestScopePropagator.class);
bind(HttpRequestContext.class);
- install(new RunAsFilter.Module());
-
installAuthModule();
if (options.enableMasterFeatures()) {
install(new UrlModule(options, authConfig));
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index 2ed4c36..b3813f6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -49,6 +49,10 @@
usage = "Batch mode; skip interactive prompting")
private boolean batchMode;
+ @Option(name = "--delete-caches",
+ usage = "Delete all persistent caches without asking")
+ private boolean deleteCaches;
+
@Option(name = "--no-auto-start", usage = "Don't automatically start daemon after init")
private boolean noAutoStart;
@@ -160,6 +164,12 @@
}
@Override
+ protected boolean getDeleteCaches() {
+ return deleteCaches;
+ }
+
+
+ @Override
protected boolean skipPlugins() {
return skipPlugins;
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
index 36163b6..8ccdebc 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -119,6 +119,8 @@
init.flags.autoStart = getAutoStart() && init.site.isNew;
init.flags.dev = isDev() && init.site.isNew;
init.flags.skipPlugins = skipPlugins();
+ init.flags.deleteCaches = getDeleteCaches();
+
final SiteRun run;
try {
@@ -471,4 +473,8 @@
protected boolean isDev() {
return false;
}
+
+ protected boolean getDeleteCaches() {
+ return false;
+ }
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
index b5b230d..aac2b36 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
@@ -15,28 +15,41 @@
package com.google.gerrit.pgm.init;
import com.google.gerrit.common.FileUtil;
+import com.google.gerrit.pgm.init.api.ConsoleUI;
+import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.pgm.init.api.InitStep;
import com.google.gerrit.pgm.init.api.Section;
import com.google.gerrit.server.config.SitePaths;
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
/** Initialize the {@code cache} configuration section. */
@Singleton
class InitCache implements InitStep {
+ private final ConsoleUI ui;
+ private final InitFlags flags;
private final SitePaths site;
private final Section cache;
@Inject
- InitCache(final SitePaths site, final Section.Factory sections) {
+ InitCache(final ConsoleUI ui, final InitFlags flags,
+ final SitePaths site, final Section.Factory sections) {
+ this.ui = ui;
+ this.flags = flags;
this.site = site;
this.cache = sections.get("cache", null);
}
@Override
public void run() {
+ ui.header("Cache");
String path = cache.get("directory");
if (path != null && path.isEmpty()) {
@@ -53,5 +66,27 @@
Path loc = site.resolve(path);
FileUtil.mkdirsOrDie(loc, "cannot create cache.directory");
+ List<Path> cacheFiles = new ArrayList<>();
+ try (DirectoryStream<Path> stream =
+ Files.newDirectoryStream(loc, "*.{lock,h2,trace}.db")) {
+ for (Path entry : stream) {
+ cacheFiles.add(entry);
+ }
+ } catch (IOException e) {
+ ui.message("IO error during cache directory scan");
+ return;
+ }
+ if (!cacheFiles.isEmpty()) {
+ for (Path entry : cacheFiles) {
+ if (flags.deleteCaches ||
+ ui.yesno(false, "Delete cache file %s", entry)) {
+ try {
+ Files.deleteIfExists(entry);
+ } catch (IOException e) {
+ ui.message("Could not delete " + entry);
+ }
+ }
+ }
+ }
}
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDev.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDev.java
new file mode 100644
index 0000000..5500da8
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDev.java
@@ -0,0 +1,42 @@
+// 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.pgm.init;
+
+import com.google.gerrit.pgm.init.api.InitFlags;
+import com.google.gerrit.pgm.init.api.InitStep;
+import com.google.gerrit.pgm.init.api.Section;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class InitDev implements InitStep {
+ private final InitFlags flags;
+ private final Section plugins;
+
+ @Inject
+ InitDev(InitFlags flags,
+ Section.Factory sections) {
+ this.flags = flags;
+ this.plugins = sections.get("plugins", null);
+ }
+
+ @Override
+ public void run() throws Exception {
+ if (!flags.dev) {
+ return;
+ }
+ plugins.set("allowRemoteAdmin", "true");
+ }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
index b5aa625..a442f29 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
@@ -64,6 +64,7 @@
step().to(InitHttpd.class);
step().to(InitCache.class);
step().to(InitPlugins.class);
+ step().to(InitDev.class);
}
protected LinkedBindingBuilder<InitStep> step() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index 1d7195b..c3d516b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -115,6 +115,7 @@
extractMailExample("FooterHtml.soy");
extractMailExample("HeaderHtml.soy");
extractMailExample("Merged.soy");
+ extractMailExample("MergedHtml.soy");
extractMailExample("NewChange.soy");
extractMailExample("NewChangeHtml.soy");
extractMailExample("RegisterNewEmail.soy");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
index f0674d6..fa62d93 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
@@ -39,6 +39,9 @@
/** Skip plugins */
public boolean skipPlugins;
+ /** Delete all cache files */
+ public boolean deleteCaches;
+
/** Dev mode */
public boolean dev;
diff --git a/gerrit-prettify/BUILD b/gerrit-prettify/BUILD
index 063feee..b8d4dd6 100644
--- a/gerrit-prettify/BUILD
+++ b/gerrit-prettify/BUILD
@@ -33,3 +33,8 @@
],
visibility = ['//visibility:public'],
)
+
+exports_files([
+ 'src/main/resources/com/google/gerrit/prettify/client/prettify.css',
+ 'src/main/resources/com/google/gerrit/prettify/client/prettify.js',
+])
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
index 898dc94..db44d33 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
@@ -18,6 +18,7 @@
import com.google.gwtorm.client.StringKey;
import java.sql.Timestamp;
+import java.util.Objects;
/** A message attached to a {@link Change}. */
public final class ChangeMessage {
@@ -78,6 +79,13 @@
@Column(id = 6, notNull = false)
protected String tag;
+ /**
+ * Real user that added this message on behalf of the user recorded in {@link
+ * #author}.
+ */
+ @Column(id = 7, notNull = false)
+ protected Account.Id realAuthor;
+
protected ChangeMessage() {
}
@@ -105,6 +113,15 @@
author = accountId;
}
+ public Account.Id getRealAuthor() {
+ return realAuthor != null ? realAuthor : getAuthor();
+ }
+
+ public void setRealAuthor(Account.Id id) {
+ // Use null for same real author, as before the column was added.
+ realAuthor = Objects.equals(getAuthor(), id) ? null : id;
+ }
+
public Timestamp getWrittenOn() {
return writtenOn;
}
@@ -142,6 +159,7 @@
return "ChangeMessage{"
+ "key=" + key
+ ", author=" + author
+ + ", realAuthor=" + realAuthor
+ ", writtenOn=" + writtenOn
+ ", patchset=" + patchset
+ ", tag=" + tag
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Comment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Comment.java
index f37b6bd..5ec3e47 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Comment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Comment.java
@@ -181,6 +181,7 @@
public Key key;
public int lineNbr;
public Identity author;
+ protected Identity realAuthor;
public Timestamp writtenOn;
public short side;
public String message;
@@ -194,6 +195,7 @@
this(new Key(c.key), c.author.getId(), new Timestamp(c.writtenOn.getTime()),
c.side, c.message, c.serverId);
this.lineNbr = c.lineNbr;
+ this.realAuthor = c.realAuthor;
this.range = c.range != null ? new Range(c.range) : null;
this.tag = c.tag;
this.revId = c.revId;
@@ -203,6 +205,7 @@
short side, String message, String serverId) {
this.key = key;
this.author = new Comment.Identity(author);
+ this.realAuthor = this.author;
this.writtenOn = writtenOn;
this.side = side;
this.message = message;
@@ -229,6 +232,16 @@
this.revId = revId != null ? revId.get() : null;
}
+ public void setRealAuthor(Account.Id id) {
+ realAuthor = id != null && id.get() != author.id
+ ? new Comment.Identity(id)
+ : null;
+ }
+
+ public Identity getRealAuthor() {
+ return realAuthor != null ? realAuthor : author;
+ }
+
@Override
public boolean equals(Object o) {
if (o instanceof Comment) {
@@ -249,6 +262,9 @@
.append("key=").append(key).append(',')
.append("lineNbr=").append(lineNbr).append(',')
.append("author=").append(author.getId().get()).append(',')
+ .append("realAuthor=")
+ .append(realAuthor != null ? realAuthor.getId().get() : "")
+ .append(',')
.append("writtenOn=").append(writtenOn.toString()).append(',')
.append("side=").append(side).append(',')
.append("message=").append(Objects.toString(message, "")).append(',')
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index 5a3cc16..5d2f3bb 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -123,6 +123,7 @@
plc.setTag(c.tag);
plc.setRevId(new RevId(c.revId));
plc.setStatus(status);
+ plc.setRealAuthor(c.getRealAuthor().getId());
return plc;
}
@@ -167,6 +168,13 @@
protected String tag;
/**
+ * Real user that added this comment on behalf of the user recorded in {@link
+ * #author}.
+ */
+ @Column(id = 11, notNull = false)
+ protected Account.Id realAuthor;
+
+ /**
* The RevId for the commit to which this comment is referring.
*
* Note that this field is not stored in the database. It is just provided
@@ -192,6 +200,7 @@
key = o.key;
lineNbr = o.lineNbr;
author = o.author;
+ realAuthor = o.realAuthor;
writtenOn = o.writtenOn;
status = o.status;
side = o.side;
@@ -227,6 +236,15 @@
return author;
}
+ public Account.Id getRealAuthor() {
+ return realAuthor != null ? realAuthor : getAuthor();
+ }
+
+ public void setRealAuthor(Account.Id id) {
+ // Use null for same real author, as before the column was added.
+ realAuthor = Objects.equals(getAuthor(), id) ? null : id;
+ }
+
public Timestamp getWrittenOn() {
return writtenOn;
}
@@ -309,6 +327,7 @@
c.lineNbr = lineNbr;
c.parentUuid = parentUuid;
c.tag = tag;
+ c.setRealAuthor(getRealAuthor());
return c;
}
@@ -343,6 +362,8 @@
builder.append("key=").append(key).append(',');
builder.append("lineNbr=").append(lineNbr).append(',');
builder.append("author=").append(author.get()).append(',');
+ builder.append("realAuthor=")
+ .append(realAuthor != null ? realAuthor.get() : "").append(',');
builder.append("writtenOn=").append(writtenOn.toString()).append(',');
builder.append("status=").append(status).append(',');
builder.append("side=").append(side).append(',');
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
index a8bf07b..2210319 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -201,6 +201,16 @@
id = k;
}
+ public PatchSet(PatchSet src) {
+ this.id = src.id;
+ this.revision = src.revision;
+ this.uploader = src.uploader;
+ this.createdOn = src.createdOn;
+ this.draft = src.draft;
+ this.groups = src.groups;
+ this.pushCertificate = src.pushCertificate;
+ }
+
public PatchSet.Id getId() {
return id;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
index 1d0d29b..a2becc2 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
@@ -93,6 +93,13 @@
@Column(id = 6, notNull = false)
protected String tag;
+ /**
+ * Real user that made this approval on behalf of the user recorded in {@link
+ * Key#accountId}.
+ */
+ @Column(id = 7, notNull = false)
+ protected Account.Id realAccountId;
+
// DELETED: id = 4 (changeOpen)
// DELETED: id = 5 (changeSortKey)
@@ -110,6 +117,12 @@
new PatchSetApproval.Key(psId, src.getAccountId(), src.getLabelId());
value = src.getValue();
granted = src.granted;
+ realAccountId = src.realAccountId;
+ tag = src.tag;
+ }
+
+ public PatchSetApproval(PatchSetApproval src) {
+ this(src.getPatchSetId(), src);
}
public PatchSetApproval.Key getKey() {
@@ -124,6 +137,15 @@
return key.accountId;
}
+ public Account.Id getRealAccountId() {
+ return realAccountId != null ? realAccountId : getAccountId();
+ }
+
+ public void setRealAccountId(Account.Id id) {
+ // Use null for same real author, as before the column was added.
+ realAccountId = Objects.equals(getAccountId(), id) ? null : id;
+ }
+
public LabelId getLabelId() {
return key.categoryId;
}
@@ -166,8 +188,12 @@
@Override
public String toString() {
- return new StringBuilder().append('[').append(key).append(": ")
- .append(value).append(",tag:").append(tag).append(']').toString();
+ return "["
+ + key + ": "
+ + value
+ + ",tag:" + tag
+ + ",realAccountId:" + realAccountId
+ + ']';
}
@Override
@@ -177,7 +203,8 @@
return Objects.equals(key, p.key)
&& Objects.equals(value, p.value)
&& Objects.equals(granted, p.granted)
- && Objects.equals(tag, p.tag);
+ && Objects.equals(tag, p.tag)
+ && Objects.equals(realAccountId, p.realAccountId);
}
return false;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RobotComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RobotComment.java
index da9584d..ecb952a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RobotComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RobotComment.java
@@ -15,12 +15,14 @@
package com.google.gerrit.reviewdb.client;
import java.sql.Timestamp;
+import java.util.Map;
import java.util.Objects;
public class RobotComment extends Comment {
public String robotId;
public String robotRunId;
public String url;
+ public Map<String, String> properties;
public RobotComment(Key key, Account.Id author, Timestamp writtenOn,
short side, String message, String serverId, String robotId,
@@ -39,6 +41,9 @@
.append("robotRunId=").append(robotRunId).append(',')
.append("lineNbr=").append(lineNbr).append(',')
.append("author=").append(author.getId().get()).append(',')
+ .append("realAuthor=")
+ .append(realAuthor != null ? realAuthor.getId().get() : "")
+ .append(',')
.append("writtenOn=").append(writtenOn.toString()).append(',')
.append("side=").append(side).append(',')
.append("message=").append(Objects.toString(message, "")).append(',')
@@ -47,7 +52,8 @@
.append("range=").append(Objects.toString(range, "")).append(',')
.append("revId=").append(revId != null ? revId : "").append(',')
.append("tag=").append(Objects.toString(tag, "")).append(',')
- .append("url=").append(url)
+ .append("url=").append(url).append(',')
+ .append("properties=").append(properties != null ? properties : "")
.append('}')
.toString();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 43f20cc..67f07bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server;
+import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static java.util.Comparator.comparing;
@@ -26,6 +27,7 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
+import com.google.common.primitives.Shorts;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.Permission;
@@ -86,6 +88,19 @@
return SORT_APPROVALS.sortedCopy(approvals);
}
+ public static PatchSetApproval newApproval(PatchSet.Id psId, CurrentUser user,
+ LabelId labelId, int value, Date when) {
+ PatchSetApproval psa = new PatchSetApproval(
+ new PatchSetApproval.Key(
+ psId,
+ user.getAccountId(),
+ labelId),
+ Shorts.checkedCast(value),
+ when);
+ user.updateRealAccountId(psa::setRealAccountId);
+ return psa;
+ }
+
private static Iterable<PatchSetApproval> filterApprovals(
Iterable<PatchSetApproval> psas, final Account.Id accountId) {
return Iterables.filter(
@@ -266,7 +281,7 @@
}
/**
- * Adds approvals to ChangeUpdate and writes to ReviewDb.
+ * Adds approvals to ChangeUpdate for a new patch set, and writes to ReviewDb.
*
* @param db review database.
* @param update change update.
@@ -276,9 +291,14 @@
* @param approvals approvals to add.
* @throws OrmException
*/
- public Iterable<PatchSetApproval> addApprovals(ReviewDb db, ChangeUpdate update,
- LabelTypes labelTypes, PatchSet ps, ChangeControl changeCtl,
- Map<String, Short> approvals) throws OrmException {
+ public Iterable<PatchSetApproval> addApprovalsForNewPatchSet(ReviewDb db,
+ ChangeUpdate update, LabelTypes labelTypes, PatchSet ps,
+ ChangeControl changeCtl, Map<String, Short> approvals)
+ throws OrmException {
+ Account.Id accountId = changeCtl.getUser().getAccountId();
+ checkArgument(accountId.equals(ps.getUploader()),
+ "expected user %s to match patch set uploader %s",
+ accountId, ps.getUploader());
if (approvals.isEmpty()) {
return Collections.emptyList();
}
@@ -287,12 +307,9 @@
Date ts = update.getWhen();
for (Map.Entry<String, Short> vote : approvals.entrySet()) {
LabelType lt = labelTypes.byLabel(vote.getKey());
- cells.add(new PatchSetApproval(new PatchSetApproval.Key(
- ps.getId(),
- ps.getUploader(),
- lt.getLabelId()),
- vote.getValue(),
- ts));
+ cells.add(
+ newApproval(ps.getId(), changeCtl.getUser(), lt.getLabelId(),
+ vote.getValue(), ts));
}
for (PatchSetApproval psa : cells) {
update.putApproval(psa.getLabel(), psa.getValue());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
index f3fdbcb..13b289f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -14,12 +14,15 @@
package com.google.gerrit.server;
+import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.NotesMigration;
@@ -27,6 +30,7 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
+import java.sql.Timestamp;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -39,6 +43,26 @@
*/
@Singleton
public class ChangeMessagesUtil {
+ public static ChangeMessage newMessage(BatchUpdate.ChangeContext ctx,
+ String body) throws OrmException {
+ return newMessage(
+ ctx.getDb(), ctx.getChange().currentPatchSetId(),
+ ctx.getUser(), ctx.getWhen(), body);
+ }
+
+ public static ChangeMessage newMessage(
+ ReviewDb db, PatchSet.Id psId, CurrentUser user, Timestamp when,
+ String body) throws OrmException {
+ checkNotNull(psId);
+ Account.Id accountId = user.isInternalUser() ? null : user.getAccountId();
+ ChangeMessage m = new ChangeMessage(
+ new ChangeMessage.Key(psId.getParentKey(), ChangeUtil.messageUUID(db)),
+ accountId, when, psId);
+ m.setMessage(body);
+ user.updateRealAccountId(m::setRealAuthor);
+ return m;
+ }
+
private static List<ChangeMessage> sortChangeMessages(
Iterable<ChangeMessage> changeMessage) {
return ChangeNotes.MESSAGE_BY_TIME.sortedCopy(changeMessage);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
index 8197689..81ec4eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
@@ -39,6 +39,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerId;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -129,6 +130,26 @@
this.serverId = serverId;
}
+ public Comment newComment(ChangeContext ctx, String path, PatchSet.Id psId,
+ short side, String message) throws OrmException {
+ Comment c = new Comment(
+ new Comment.Key(ChangeUtil.messageUUID(ctx.getDb()), path, psId.get()),
+ ctx.getUser().getAccountId(), ctx.getWhen(), side, message, serverId);
+ ctx.getUser().updateRealAccountId(c::setRealAuthor);
+ return c;
+ }
+
+ public RobotComment newRobotComment(ChangeContext ctx, String path,
+ PatchSet.Id psId, short side, String message, String robotId,
+ String robotRunId) throws OrmException {
+ RobotComment c = new RobotComment(
+ new Comment.Key(ChangeUtil.messageUUID(ctx.getDb()), path, psId.get()),
+ ctx.getUser().getAccountId(), ctx.getWhen(), side, message, serverId,
+ robotId, robotRunId);
+ ctx.getUser().updateRealAccountId(c::setRealAuthor);
+ return c;
+ }
+
public Optional<Comment> get(ReviewDb db, ChangeNotes notes,
Comment.Key key) throws OrmException {
if (!migration.readChanges()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index 34a2d02..668b344 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -20,6 +20,8 @@
import com.google.gerrit.server.account.GroupMembership;
import com.google.inject.servlet.RequestScoped;
+import java.util.function.Consumer;
+
/**
* Information about the currently logged in user.
* <p>
@@ -72,6 +74,16 @@
}
/**
+ * If the {@link #getRealUser()} has an account ID associated with it, call
+ * the given setter with that ID.
+ */
+ public void updateRealAccountId(Consumer<Account.Id> setter) {
+ if (getRealUser().isIdentifiedUser()) {
+ setter.accept(getRealUser().getAccountId());
+ }
+ }
+
+ /**
* Get the set of groups the user is currently a member of.
* <p>
* The returned set may be a subset of the user's actual groups; if the user's
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
new file mode 100644
index 0000000..69f294d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
@@ -0,0 +1,269 @@
+// 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;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.account.AccountDirectory.FillOptions;
+import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.change.ReviewerSuggestion;
+import com.google.gerrit.server.change.SuggestReviewers;
+import com.google.gerrit.server.change.SuggestedReviewer;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.apache.commons.lang.mutable.MutableDouble;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class ReviewerRecommender {
+ private static final Logger log =
+ LoggerFactory.getLogger(ReviewersUtil.class);
+ private static final double BASE_REVIEWER_WEIGHT = 10;
+ private static final double BASE_OWNER_WEIGHT = 1;
+ private static final double BASE_COMMENT_WEIGHT = 0.5;
+ private static final double[] WEIGHTS = new double[] {
+ BASE_REVIEWER_WEIGHT, BASE_OWNER_WEIGHT, BASE_COMMENT_WEIGHT,};
+ private static final long PLUGIN_QUERY_TIMEOUT = 500; //ms
+
+ private final ChangeQueryBuilder changeQueryBuilder;
+ private final Config config;
+ private final DynamicMap<ReviewerSuggestion> reviewerSuggestionPluginMap;
+ private final InternalChangeQuery internalChangeQuery;
+ private final WorkQueue workQueue;
+
+ @Inject
+ ReviewerRecommender(ChangeQueryBuilder changeQueryBuilder,
+ DynamicMap<ReviewerSuggestion> reviewerSuggestionPluginMap,
+ InternalChangeQuery internalChangeQuery,
+ WorkQueue workQueue,
+ @GerritServerConfig Config config) {
+ Set<FillOptions> fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS);
+ fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
+ this.changeQueryBuilder = changeQueryBuilder;
+ this.config = config;
+ this.internalChangeQuery = internalChangeQuery;
+ this.reviewerSuggestionPluginMap = reviewerSuggestionPluginMap;
+ this.workQueue = workQueue;
+ }
+
+ public List<Account.Id> suggestReviewers(
+ ChangeNotes changeNotes,
+ SuggestReviewers suggestReviewers, ProjectControl projectControl,
+ List<Account.Id> candidateList)
+ throws OrmException {
+ String query = suggestReviewers.getQuery();
+ double baseWeight = config.getInt("addReviewer", "baseWeight", 1);
+
+ Map<Account.Id, MutableDouble> reviewerScores;
+ if (Strings.isNullOrEmpty(query)) {
+ reviewerScores = baseRankingForEmptyQuery(baseWeight);
+ } else {
+ reviewerScores = baseRankingForCandidateList(
+ candidateList, projectControl, baseWeight);
+ }
+
+ // Send the query along with a candidate list to all plugins and merge the
+ // results. Plugins don't necessarily need to use the candidates list, they
+ // can also return non-candidate account ids.
+ List<Callable<Set<SuggestedReviewer>>> tasks =
+ new ArrayList<>(reviewerSuggestionPluginMap.plugins().size());
+ List<Double> weights =
+ new ArrayList<>(reviewerSuggestionPluginMap.plugins().size());
+
+ for (DynamicMap.Entry<ReviewerSuggestion> plugin :
+ reviewerSuggestionPluginMap) {
+ tasks.add(() -> plugin.getProvider().get()
+ .suggestReviewers(projectControl.getProject().getNameKey(),
+ changeNotes.getChangeId(), query, reviewerScores.keySet()));
+ String pluginWeight = config.getString("addReviewer",
+ plugin.getPluginName() + "-" + plugin.getExportName(), "weight");
+ if (Strings.isNullOrEmpty(pluginWeight)) {
+ pluginWeight = "1";
+ }
+ try {
+ weights.add(Double.parseDouble(pluginWeight));
+ } catch (NumberFormatException e) {
+ log.error("Exception while parsing weight for " +
+ plugin.getPluginName() + "-" + plugin.getExportName(), e);
+ weights.add(1d);
+ }
+ }
+
+ try {
+ List<Future<Set<SuggestedReviewer>>> futures = workQueue
+ .getDefaultQueue()
+ .invokeAll(tasks, PLUGIN_QUERY_TIMEOUT, TimeUnit.MILLISECONDS);
+ Iterator<Double> weightIterator = weights.iterator();
+ for (Future<Set<SuggestedReviewer>> f : futures) {
+ double weight = weightIterator.next();
+ for (SuggestedReviewer s : f.get()) {
+ if (reviewerScores.containsKey(s.account)) {
+ reviewerScores.get(s.account).add(s.score * weight);
+ } else {
+ reviewerScores.put(s.account, new MutableDouble(s.score * weight));
+ }
+ }
+ }
+ } catch (ExecutionException | InterruptedException e) {
+ log.error("Exception while suggesting reviewers", e);
+ return ImmutableList.of();
+ }
+
+ // Remove change owner
+ if (changeNotes != null) {
+ reviewerScores.remove(changeNotes.getChange().getOwner());
+ }
+
+ // Sort results
+ Stream<Entry<Account.Id, MutableDouble>> sorted =
+ reviewerScores.entrySet().stream()
+ .sorted(Collections.reverseOrder(Map.Entry.comparingByValue()));
+ List<Account.Id> sortedSuggestions = sorted
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toList());
+ return sortedSuggestions;
+ }
+
+ private Map<Account.Id, MutableDouble> baseRankingForEmptyQuery(
+ double baseWeight) throws OrmException{
+ // Get the user's last 50 changes, check approvals
+ try {
+ List<ChangeData> result = internalChangeQuery
+ .setLimit(50)
+ .setRequestedFields(ImmutableSet.of(ChangeField.REVIEWER.getName()))
+ .query(changeQueryBuilder.owner("self"));
+ Map<Account.Id, MutableDouble> suggestions = new HashMap<>();
+ for (ChangeData cd : result) {
+ for (PatchSetApproval approval : cd.currentApprovals()) {
+ Account.Id id = approval.getAccountId();
+ if (suggestions.containsKey(id)) {
+ suggestions.get(id).add(baseWeight);
+ } else {
+ suggestions.put(id, new MutableDouble(baseWeight));
+ }
+ }
+ }
+ return suggestions;
+ } catch (QueryParseException e) {
+ // Unhandled, because owner:self will never provoke a QueryParseException
+ log.error("Exception while suggesting reviewers", e);
+ return ImmutableMap.of();
+ }
+ }
+
+ private Map<Account.Id, MutableDouble> baseRankingForCandidateList(
+ List<Account.Id> candidates,
+ ProjectControl projectControl,
+ double baseWeight) throws OrmException {
+ // Get each reviewer's activity based on number of applied labels
+ // (weighted 10d), number of comments (weighted 0.5d) and number of owned
+ // changes (weighted 1d).
+ Map<Account.Id, MutableDouble> reviewers = new LinkedHashMap<>();
+ if (candidates.size() == 0) {
+ return reviewers;
+ }
+ List<Predicate<ChangeData>> predicates = new ArrayList<>();
+ for (Account.Id id : candidates) {
+ try {
+ Predicate<ChangeData> projectQuery =
+ changeQueryBuilder.project(projectControl.getProject().getName());
+
+ // Get all labels for this project and create a compound OR query to
+ // fetch all changes where users have applied one of these labels
+ List<LabelType> labelTypes =
+ projectControl.getLabelTypes().getLabelTypes();
+ List<Predicate<ChangeData>> labelPredicates =
+ new ArrayList<>(labelTypes.size());
+ for (LabelType type : labelTypes) {
+ labelPredicates
+ .add(changeQueryBuilder.label(type.getName() + ",user=" + id));
+ }
+ Predicate<ChangeData> reviewerQuery =
+ Predicate.and(projectQuery, Predicate.or(labelPredicates));
+
+ Predicate<ChangeData> ownerQuery = Predicate.and(projectQuery,
+ changeQueryBuilder.owner(id.toString()));
+ Predicate<ChangeData> commentedByQuery = Predicate.and(projectQuery,
+ changeQueryBuilder.commentby(id.toString()));
+
+ predicates.add(reviewerQuery);
+ predicates.add(ownerQuery);
+ predicates.add(commentedByQuery);
+ reviewers.put(id, new MutableDouble());
+ } catch (QueryParseException e) {
+ // Unhandled: If an exception is thrown, we won't increase the
+ // candidates's score
+ log.error("Exception while suggesting reviewers", e);
+ }
+ }
+
+ List<List<ChangeData>> result = internalChangeQuery
+ .setLimit(100 * predicates.size())
+ .setRequestedFields(ImmutableSet.of())
+ .query(predicates);
+
+ Iterator<List<ChangeData>> queryResultIterator = result.iterator();
+ Iterator<Account.Id> reviewersIterator = reviewers.keySet().iterator();
+
+ int i = 0;
+ Account.Id currentId = null;
+ while (queryResultIterator.hasNext()) {
+ List<ChangeData> currentResult = queryResultIterator.next();
+ if (i % WEIGHTS.length == 0) {
+ currentId = reviewersIterator.next();
+ }
+
+ reviewers.get(currentId).add(WEIGHTS[i % WEIGHTS.length] *
+ baseWeight * currentResult.size());
+ i++;
+ }
+ return reviewers;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
index b01c233..1781f1a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -14,20 +14,15 @@
package com.google.gerrit.server;
-import static java.util.Comparator.comparing;
-
-import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
-import com.google.common.collect.Ordering;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.GroupBaseInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
-import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -44,6 +39,7 @@
import com.google.gerrit.server.change.SuggestReviewers;
import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.account.AccountIndexCollection;
+import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.query.QueryParseException;
@@ -56,98 +52,95 @@
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
+import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
public class ReviewersUtil {
private static final String MAX_SUFFIX = "\u9fa5";
- private static final Ordering<SuggestedReviewerInfo> ORDERING =
- Ordering.<SuggestedReviewerInfo> from(comparing(
- suggestedReviewerInfo -> {
- if (suggestedReviewerInfo == null) {
- return null;
- }
- return suggestedReviewerInfo.account != null
- ? MoreObjects.firstNonNull(suggestedReviewerInfo.account.email,
- Strings.nullToEmpty(suggestedReviewerInfo.account.name))
- : Strings.nullToEmpty(suggestedReviewerInfo.group.name);
- }));
+ // Generate a candidate list at 3x the size of what the user wants to see to
+ // give the ranking algorithm a good set of candidates it can work with
+ private static final int CANDIDATE_LIST_MULTIPLIER = 3;
- private final AccountLoader accountLoader;
private final AccountCache accountCache;
- private final AccountIndexCollection indexes;
- private final AccountQueryBuilder queryBuilder;
- private final AccountQueryProcessor queryProcessor;
private final AccountControl accountControl;
- private final Provider<ReviewDb> dbProvider;
+ private final AccountIndexCollection accountIndexes;
+ private final AccountLoader accountLoader;
+ private final AccountQueryBuilder accountQueryBuilder;
+ private final AccountQueryProcessor accountQueryProcessor;
private final GroupBackend groupBackend;
private final GroupMembers.Factory groupMembersFactory;
private final Provider<CurrentUser> currentUser;
+ private final Provider<ReviewDb> dbProvider;
+ private final ReviewerRecommender reviewerRecommender;
@Inject
- ReviewersUtil(AccountLoader.Factory accountLoaderFactory,
- AccountCache accountCache,
- AccountIndexCollection indexes,
- AccountQueryBuilder queryBuilder,
- AccountQueryProcessor queryProcessor,
+ ReviewersUtil(AccountCache accountCache,
AccountControl.Factory accountControlFactory,
- Provider<ReviewDb> dbProvider,
+ AccountIndexCollection accountIndexes,
+ AccountLoader.Factory accountLoaderFactory,
+ AccountQueryBuilder accountQueryBuilder,
+ AccountQueryProcessor accountQueryProcessor,
GroupBackend groupBackend,
GroupMembers.Factory groupMembersFactory,
- Provider<CurrentUser> currentUser) {
+ Provider<CurrentUser> currentUser,
+ Provider<ReviewDb> dbProvider,
+ ReviewerRecommender reviewerRecommender) {
Set<FillOptions> fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS);
fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
- this.accountLoader = accountLoaderFactory.create(fillOptions);
this.accountCache = accountCache;
- this.indexes = indexes;
- this.queryBuilder = queryBuilder;
- this.queryProcessor = queryProcessor;
this.accountControl = accountControlFactory.get();
+ this.accountIndexes = accountIndexes;
+ this.accountLoader = accountLoaderFactory.create(fillOptions);
+ this.accountQueryBuilder = accountQueryBuilder;
+ this.accountQueryProcessor = accountQueryProcessor;
+ this.currentUser = currentUser;
this.dbProvider = dbProvider;
this.groupBackend = groupBackend;
this.groupMembersFactory = groupMembersFactory;
- this.currentUser = currentUser;
+ this.reviewerRecommender = reviewerRecommender;
}
public interface VisibilityControl {
boolean isVisibleTo(Account.Id account) throws OrmException;
}
- public List<SuggestedReviewerInfo> suggestReviewers(
+ public List<SuggestedReviewerInfo> suggestReviewers(ChangeNotes changeNotes,
SuggestReviewers suggestReviewers, ProjectControl projectControl,
VisibilityControl visibilityControl, boolean excludeGroups)
- throws IOException, OrmException, BadRequestException {
+ throws IOException, OrmException {
String query = suggestReviewers.getQuery();
- boolean suggestAccounts = suggestReviewers.getSuggestAccounts();
- int suggestFrom = suggestReviewers.getSuggestFrom();
int limit = suggestReviewers.getLimit();
- if (Strings.isNullOrEmpty(query)) {
- throw new BadRequestException("missing query field");
- }
-
- if (!suggestAccounts || query.length() < suggestFrom) {
+ if (!suggestReviewers.getSuggestAccounts()) {
return Collections.emptyList();
}
- Collection<AccountInfo> suggestedAccounts =
- suggestAccounts(suggestReviewers, visibilityControl);
-
- List<SuggestedReviewerInfo> reviewer = new ArrayList<>();
- for (AccountInfo a : suggestedAccounts) {
- SuggestedReviewerInfo info = new SuggestedReviewerInfo();
- info.account = a;
- info.count = 1;
- reviewer.add(info);
+ List<Account.Id> candidateList = new ArrayList<>();
+ if (!Strings.isNullOrEmpty(query)) {
+ candidateList = suggestAccounts(suggestReviewers, visibilityControl);
}
- if (!excludeGroups) {
+ List<Account.Id> sortedRecommendations = reviewerRecommender
+ .suggestReviewers(changeNotes, suggestReviewers, projectControl,
+ candidateList);
+
+ // Populate AccountInfo
+ List<SuggestedReviewerInfo> reviewer = new ArrayList<>();
+ for (Account.Id id : sortedRecommendations) {
+ AccountInfo account = accountLoader.get(id);
+ if (account != null) {
+ SuggestedReviewerInfo info = new SuggestedReviewerInfo();
+ info.account = account;
+ info.count = 1;
+ reviewer.add(info);
+ }
+ }
+ accountLoader.fill();
+
+ if (!excludeGroups && !Strings.isNullOrEmpty(query)) {
for (GroupReference g : suggestAccountGroup(suggestReviewers, projectControl)) {
GroupAsReviewer result = suggestGroupAsReviewer(
suggestReviewers, projectControl.getProject(), g, visibilityControl);
@@ -161,59 +154,56 @@
if (result.allowedWithConfirmation) {
suggestedReviewerInfo.confirm = true;
}
+ // Always add groups at the end as individual accounts are usually
+ // more important
reviewer.add(suggestedReviewerInfo);
}
}
}
- reviewer = ORDERING.immutableSortedCopy(reviewer);
if (reviewer.size() <= limit) {
return reviewer;
}
return reviewer.subList(0, limit);
}
- private Collection<AccountInfo> suggestAccounts(SuggestReviewers suggestReviewers,
+ private List<Account.Id> suggestAccounts(SuggestReviewers suggestReviewers,
VisibilityControl visibilityControl)
throws OrmException {
- AccountIndex searchIndex = indexes.getSearchIndex();
+ AccountIndex searchIndex = accountIndexes.getSearchIndex();
if (searchIndex != null) {
return suggestAccountsFromIndex(suggestReviewers);
}
return suggestAccountsFromDb(suggestReviewers, visibilityControl);
}
- private Collection<AccountInfo> suggestAccountsFromIndex(
+ private List<Account.Id> suggestAccountsFromIndex(
SuggestReviewers suggestReviewers) throws OrmException {
try {
- Map<Account.Id, AccountInfo> matches = new LinkedHashMap<>();
- QueryResult<AccountState> result = queryProcessor
- .setLimit(suggestReviewers.getLimit())
- .query(queryBuilder.defaultQuery(suggestReviewers.getQuery()));
+ Set<Account.Id> matches = new HashSet<>();
+ QueryResult<AccountState> result = accountQueryProcessor
+ .setLimit(suggestReviewers.getLimit() * CANDIDATE_LIST_MULTIPLIER)
+ .query(accountQueryBuilder.defaultQuery(suggestReviewers.getQuery()));
for (AccountState accountState : result.entities()) {
Account.Id id = accountState.getAccount().getId();
- matches.put(id, accountLoader.get(id));
+ matches.add(id);
}
-
- accountLoader.fill();
-
- return matches.values();
+ return new ArrayList<>(matches);
} catch (QueryParseException e) {
return ImmutableList.of();
}
}
- private Collection<AccountInfo> suggestAccountsFromDb(
+ private List<Account.Id> suggestAccountsFromDb(
SuggestReviewers suggestReviewers, VisibilityControl visibilityControl)
throws OrmException {
String query = suggestReviewers.getQuery();
- int limit = suggestReviewers.getLimit();
+ int limit = suggestReviewers.getLimit() * CANDIDATE_LIST_MULTIPLIER;
String a = query;
String b = a + MAX_SUFFIX;
- Map<Account.Id, AccountInfo> r = new LinkedHashMap<>();
- Map<Account.Id, String> queryEmail = new HashMap<>();
+ Set<Account.Id> r = new HashSet<>();
for (Account p : dbProvider.get().accounts()
.suggestByFullName(a, b, limit)) {
@@ -234,36 +224,26 @@
if (r.size() < limit) {
for (AccountExternalId e : dbProvider.get().accountExternalIds()
.suggestByEmailAddress(a, b, limit - r.size())) {
- if (!r.containsKey(e.getAccountId())) {
+ if (!r.contains(e.getAccountId())) {
Account p = accountCache.get(e.getAccountId()).getAccount();
if (p.isActive()) {
- if (addSuggestion(r, p.getId(), visibilityControl)) {
- queryEmail.put(e.getAccountId(), e.getEmailAddress());
- }
+ addSuggestion(r, p.getId(), visibilityControl);
}
}
}
}
-
- accountLoader.fill();
- for (Map.Entry<Account.Id, String> p : queryEmail.entrySet()) {
- AccountInfo info = r.get(p.getKey());
- if (info != null) {
- info.email = p.getValue();
- }
- }
- return new ArrayList<>(r.values());
+ return new ArrayList<>(r);
}
- private boolean addSuggestion(Map<Account.Id, AccountInfo> map,
+ private boolean addSuggestion(Set<Account.Id> map,
Account.Id account, VisibilityControl visibilityControl)
throws OrmException {
- if (!map.containsKey(account)
+ if (!map.contains(account)
// Can the suggestion see the change?
&& visibilityControl.isVisibleTo(account)
// Can the current user see the account?
&& accountControl.canSee(account)) {
- map.put(account, accountLoader.get(account));
+ map.add(account);
return true;
}
return false;
@@ -282,7 +262,8 @@
int size;
}
- private GroupAsReviewer suggestGroupAsReviewer(SuggestReviewers suggestReviewers,
+ private GroupAsReviewer suggestGroupAsReviewer(
+ SuggestReviewers suggestReviewers,
Project project, GroupReference group,
VisibilityControl visibilityControl) throws OrmException, IOException {
GroupAsReviewer result = new GroupAsReviewer();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
index 04ebc87..c7ce1b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.account;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AcceptsCreate;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -90,15 +91,7 @@
*/
public IdentifiedUser parse(String id) throws AuthException,
UnprocessableEntityException, OrmException {
- IdentifiedUser user = parseId(id);
- if (user == null) {
- throw new UnprocessableEntityException(String.format(
- "Account Not Found: %s", id));
- } else if (!accountControlFactory.get().canSee(user.getAccount())) {
- throw new UnprocessableEntityException(String.format(
- "Account Not Found: %s", id));
- }
- return user;
+ return parseOnBehalfOf(null, id);
}
/**
@@ -115,6 +108,29 @@
* @throws OrmException
*/
public IdentifiedUser parseId(String id) throws AuthException, OrmException {
+ 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 {
+ IdentifiedUser user = parseIdOnBehalfOf(caller, id);
+ if (user == null) {
+ throw new UnprocessableEntityException(String.format(
+ "Account Not Found: %s", id));
+ } else if (!accountControlFactory.get().canSee(user.getAccount())) {
+ throw new UnprocessableEntityException(String.format(
+ "Account Not Found: %s", id));
+ }
+ return user;
+ }
+
+ private IdentifiedUser parseIdOnBehalfOf(@Nullable CurrentUser caller,
+ String id) throws AuthException, OrmException {
if (id.equals("self")) {
CurrentUser user = self.get();
if (user.isIdentifiedUser()) {
@@ -130,7 +146,8 @@
if (match == null) {
return null;
}
- return userFactory.create(match.getId());
+ CurrentUser realUser = caller != null ? caller.getRealUser() : null;
+ return userFactory.runAs(null, match.getId(), realUser);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
index c4bd68d..45f0afe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -31,7 +31,6 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.extensions.events.ChangeAbandoned;
@@ -106,7 +105,7 @@
public Change abandon(ChangeControl control, String msgTxt,
NotifyHandling notifyHandling) throws RestApiException, UpdateException {
- Op op = new Op(control.getUser(), msgTxt, notifyHandling);
+ Op op = new Op(msgTxt, notifyHandling);
try (BatchUpdate u =
batchUpdateFactory.create(
dbProvider.get(),
@@ -142,8 +141,7 @@
control.getProject().getNameKey().get(),
project.get()));
}
- u.addOp(
- control.getId(), new Op(control.getUser(), msgTxt, notifyHandling));
+ u.addOp(control.getId(), new Op(msgTxt, notifyHandling));
}
u.execute();
}
@@ -164,18 +162,14 @@
private class Op extends BatchUpdate.Op {
private final String msgTxt;
private final NotifyHandling notifyHandling;
- private final Account account;
private Change change;
private PatchSet patchSet;
private ChangeMessage message;
- private Op(CurrentUser user, String msgTxt, NotifyHandling notifyHandling) {
+ private Op(String msgTxt, NotifyHandling notifyHandling) {
this.msgTxt = msgTxt;
this.notifyHandling = notifyHandling;
- account = user.isIdentifiedUser()
- ? user.asIdentifiedUser().getAccount()
- : null;
}
@Override
@@ -208,19 +202,14 @@
msg.append(msgTxt.trim());
}
- ChangeMessage message = new ChangeMessage(
- new ChangeMessage.Key(
- change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- account != null ? account.getId() : null,
- ctx.getWhen(),
- change.currentPatchSetId());
- message.setMessage(msg.toString());
- return message;
+ return ChangeMessagesUtil.newMessage(ctx, msg.toString());
}
@Override
public void postUpdate(Context ctx) throws OrmException {
+ Account account = ctx.getUser().isIdentifiedUser()
+ ? ctx.getUser().asIdentifiedUser().getAccount()
+ : null;
try {
ReplyToChangeSender cm =
abandonedSenderFactory.create(ctx.getProject(), change.getId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 6906fb2..6f6e964 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -34,7 +34,6 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.events.CommitReceivedEvent;
@@ -361,14 +360,12 @@
patchSetInfo,
filterOnChangeVisibility(db, ctx.getNotes(), reviewers),
Collections.<Account.Id> emptySet());
- approvalsUtil.addApprovals(db, update, labelTypes, patchSet,
- ctx.getControl(), approvals);
+ approvalsUtil.addApprovalsForNewPatchSet(
+ db, update, labelTypes, patchSet, ctx.getControl(), approvals);
if (message != null) {
- changeMessage =
- new ChangeMessage(new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(db)), ctx.getAccountId(),
- patchSet.getCreatedOn(), patchSet.getId());
- changeMessage.setMessage(message);
+ changeMessage = ChangeMessagesUtil.newMessage(
+ db, patchSet.getId(), ctx.getUser(), patchSet.getCreatedOn(),
+ message);
cmUtil.addChangeMessage(db, update, changeMessage);
}
return true;
@@ -433,7 +430,7 @@
* show a transition from an oldValue of 0 to the new value.
*/
if (fireRevisionCreated) {
- revisionCreated.fire(change, patchSet, ctx.getAccountId(),
+ revisionCreated.fire(change, patchSet, ctx.getAccount(),
ctx.getWhen(), notify);
if (approvals != null && !approvals.isEmpty()) {
ChangeControl changeControl = changeControlFactory.controlFor(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index 1a063f4..b5eb193 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -60,10 +60,12 @@
public ChangeInfo apply(RevisionResource revision, CherryPickInput input)
throws OrmException, IOException, UpdateException, RestApiException {
final ChangeControl control = revision.getControl();
+ int parent = input.parent == null ? 1 : input.parent;
if (input.message == null || input.message.trim().isEmpty()) {
throw new BadRequestException("message must be non-empty");
- } else if (input.destination == null || input.destination.trim().isEmpty()) {
+ } else if (input.destination == null
+ || input.destination.trim().isEmpty()) {
throw new BadRequestException("destination must be non-empty");
}
@@ -91,7 +93,7 @@
Change.Id cherryPickedChangeId =
cherryPickChange.cherryPick(revision.getChange(),
revision.getPatchSet(), input.message, refName,
- refControl);
+ refControl, parent);
return json.create(ChangeJson.NO_OPTIONS).format(revision.getProject(),
cherryPickedChangeId);
} catch (InvalidChangeOperationException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index 0c620e2..1a18357 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -114,8 +114,8 @@
}
public Change.Id cherryPick(Change change, PatchSet patch,
- final String message, final String ref,
- final RefControl refControl) throws NoSuchChangeException,
+ final String message, final String ref, final RefControl refControl,
+ int parent) throws NoSuchChangeException,
OrmException, MissingObjectException,
IncorrectObjectTypeException, IOException,
InvalidChangeOperationException, IntegrationException, UpdateException,
@@ -147,6 +147,13 @@
CodeReviewCommit commitToCherryPick =
revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
+ if (parent <= 0 || parent > commitToCherryPick.getParentCount()) {
+ throw new InvalidChangeOperationException(String.format(
+ "Cherry Pick: Parent %s does not exist. Please specify a parent in"
+ + " range [1, %s].",
+ parent, commitToCherryPick.getParentCount()));
+ }
+
Timestamp now = TimeUtil.nowTs();
PersonIdent committerIdent =
identifiedUser.newCommitterIdent(now, serverTimeZone);
@@ -160,10 +167,12 @@
CodeReviewCommit cherryPickCommit;
try {
- ProjectState projectState = refControl.getProjectControl().getProjectState();
- cherryPickCommit =
- mergeUtilFactory.create(projectState).createCherryPickFromCommit(git, oi, mergeTip,
- commitToCherryPick, committerIdent, commitMessage, revWalk);
+ ProjectState projectState = refControl.getProjectControl()
+ .getProjectState();
+ cherryPickCommit = mergeUtilFactory.create(projectState)
+ .createCherryPickFromCommit(git, oi, mergeTip,
+ commitToCherryPick, committerIdent, commitMessage, revWalk,
+ parent - 1);
Change.Key changeKey;
final List<String> idList = cherryPickCommit.getFooterLines(
@@ -272,10 +281,6 @@
@Override
public boolean updateChange(ChangeContext ctx) throws OrmException {
- ChangeMessage changeMessage = new ChangeMessage(
- new ChangeMessage.Key(
- ctx.getChange().getId(), ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(), psId);
StringBuilder sb = new StringBuilder("Patch Set ")
.append(psId.get())
.append(": Cherry Picked")
@@ -284,8 +289,8 @@
.append(destBranch)
.append(" as commit ")
.append(cherryPickCommit.name());
- changeMessage.setMessage(sb.toString());
-
+ ChangeMessage changeMessage = ChangeMessagesUtil.newMessage(
+ ctx.getDb(), psId, ctx.getUser(), ctx.getWhen(), sb.toString());
cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), changeMessage);
return true;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
index be019fb..54f73bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentJson.java
@@ -180,6 +180,7 @@
rci.robotId = c.robotId;
rci.robotRunId = c.robotRunId;
rci.url = c.url;
+ rci.properties = c.properties;
fillCommentInfo(c, rci, loader);
return rci;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
index 7ca8478..47821f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
@@ -30,10 +30,8 @@
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.config.GerritServerId;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.UpdateException;
@@ -53,7 +51,6 @@
private final CommentsUtil commentsUtil;
private final PatchSetUtil psUtil;
private final PatchListCache patchListCache;
- private final String serverId;
@Inject
CreateDraftComment(Provider<ReviewDb> db,
@@ -61,15 +58,13 @@
Provider<CommentJson> commentJson,
CommentsUtil commentsUtil,
PatchSetUtil psUtil,
- PatchListCache patchListCache,
- @GerritServerId String serverId) {
+ PatchListCache patchListCache) {
this.db = db;
this.updateFactory = updateFactory;
this.commentJson = commentJson;
this.commentsUtil = commentsUtil;
this.psUtil = psUtil;
this.patchListCache = patchListCache;
- this.serverId = serverId;
}
@Override
@@ -113,14 +108,8 @@
if (ps == null) {
throw new ResourceNotFoundException("patch set not found: " + psId);
}
- comment = new Comment(
- new Comment.Key(ChangeUtil.messageUUID(ctx.getDb()), in.path,
- ps.getPatchSetId()),
- ctx.getAccountId(),
- ctx.getWhen(),
- in.side(),
- in.message.trim(),
- serverId);
+ comment = commentsUtil.newComment(
+ ctx, in.path, ps.getId(), in.side(), in.message.trim());
comment.parentUuid = Url.decode(in.inReplyTo);
comment.setLineNbrAndRange(in.line, in.range);
comment.tag = in.tag;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java
index a572c58..70c2ae7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java
@@ -25,7 +25,6 @@
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
import com.google.gerrit.server.account.AccountJson;
import com.google.gerrit.server.change.DeleteAssignee.Input;
@@ -115,14 +114,8 @@
private void addMessage(BatchUpdate.ChangeContext ctx,
ChangeUpdate update, Account deleted) throws OrmException {
- ChangeMessage cmsg = new ChangeMessage(
- new ChangeMessage.Key(
- ctx.getChange().getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(),
- ctx.getChange().currentPatchSetId());
- cmsg.setMessage(
- "Assignee deleted: " + deleted.getName(anonymousCowardName));
+ ChangeMessage cmsg = ChangeMessagesUtil.newMessage(
+ ctx, "Assignee deleted: " + deleted.getName(anonymousCowardName));
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
index 099f8e0..72ffa77 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -36,7 +36,6 @@
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.extensions.events.ReviewerDeleted;
@@ -185,11 +184,7 @@
ChangeUpdate update = ctx.getUpdate(currPs.getId());
update.removeReviewer(reviewerId);
- changeMessage = new ChangeMessage(
- new ChangeMessage.Key(currChange.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(), currPs.getId());
- changeMessage.setMessage(msg.toString());
+ changeMessage = ChangeMessagesUtil.newMessage(ctx, msg.toString());
cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage);
return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
index e25e2292..eda2fcf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -35,7 +35,6 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.extensions.events.VoteDeleted;
@@ -170,20 +169,13 @@
ctx.getDb().patchSetApprovals().upsert(
Collections.singleton(deletedApproval(ctx)));
- changeMessage =
- new ChangeMessage(new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(),
- ctx.getWhen(),
- change.currentPatchSetId());
StringBuilder msg = new StringBuilder();
msg.append("Removed ");
LabelVote.appendTo(msg, label, checkNotNull(oldApprovals.get(label)));
- changeMessage.setMessage(
- msg.append(" by ")
- .append(userFactory.create(accountId).getNameEmail())
- .append("\n")
- .toString());
+ msg.append(" by ")
+ .append(userFactory.create(accountId).getNameEmail())
+ .append("\n");
+ changeMessage = ChangeMessagesUtil.newMessage(ctx, msg.toString());
cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId),
changeMessage);
@@ -191,6 +183,9 @@
}
private PatchSetApproval deletedApproval(ChangeContext ctx) {
+ // Set the effective user to the account we're trying to remove, and don't
+ // set the real user; this preserves the calling user as the NoteDb
+ // committer.
return new PatchSetApproval(
new PatchSetApproval.Key(
ps.getId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
index 2139ec4..954392c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
@@ -33,8 +33,6 @@
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -94,7 +92,7 @@
try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(),
req.getChange().getProject(), control.getUser(), TimeUtil.nowTs())) {
- u.addOp(req.getChange().getId(), new Op(control, input));
+ u.addOp(req.getChange().getId(), new Op(input));
u.execute();
}
@@ -103,14 +101,12 @@
private class Op extends BatchUpdate.Op {
private final MoveInput input;
- private final IdentifiedUser caller;
private Change change;
private Branch.NameKey newDestKey;
- Op(ChangeControl ctl, MoveInput input) {
+ Op(MoveInput input) {
this.input = input;
- this.caller = ctl.getUser().asIdentifiedUser();
}
@Override
@@ -179,11 +175,8 @@
msgBuf.append("\n\n");
msgBuf.append(input.message);
}
- ChangeMessage cmsg = new ChangeMessage(
- new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- caller.getAccountId(), ctx.getWhen(), change.currentPatchSetId());
- cmsg.setMessage(msgBuf.toString());
+ ChangeMessage cmsg =
+ ChangeMessagesUtil.newMessage(ctx, msgBuf.toString());
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 3f38fc3..9a6455c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -30,7 +30,6 @@
import com.google.gerrit.server.ApprovalCopier;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.events.CommitReceivedEvent;
@@ -225,9 +224,8 @@
}
if (message != null) {
- changeMessage = new ChangeMessage(
- new ChangeMessage.Key(ctl.getId(), ChangeUtil.messageUUID(db)),
- ctx.getAccountId(), ctx.getWhen(), patchSet.getId());
+ changeMessage = ChangeMessagesUtil.newMessage(
+ db, patchSet.getId(), ctx.getUser(), ctx.getWhen(), message);
changeMessage.setMessage(message);
}
@@ -265,7 +263,7 @@
}
if (fireRevisionCreated) {
- revisionCreated.fire(change, patchSet, ctx.getAccountId(),
+ revisionCreated.fire(change, patchSet, ctx.getAccount(),
ctx.getWhen(), notify);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 0db6f0a..cde49a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.change;
-import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
@@ -58,6 +57,7 @@
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.LabelId;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -66,13 +66,11 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.account.AccountsCollection;
-import com.google.gerrit.server.config.GerritServerId;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -121,7 +119,6 @@
private final EmailReviewComments.Factory email;
private final CommentAdded commentAdded;
private final PostReviewers postReviewers;
- private final String serverId;
private final NotesMigration migration;
@Inject
@@ -138,7 +135,6 @@
EmailReviewComments.Factory email,
CommentAdded commentAdded,
PostReviewers postReviewers,
- @GerritServerId String serverId,
NotesMigration migration) {
this.db = db;
this.batchUpdateFactory = batchUpdateFactory;
@@ -153,7 +149,6 @@
this.email = email;
this.commentAdded = commentAdded;
this.postReviewers = postReviewers;
- this.serverId = serverId;
this.migration = migration;
}
@@ -173,6 +168,8 @@
}
if (input.onBehalfOf != null) {
revision = onBehalfOf(revision, input);
+ } else if (input.drafts == null) {
+ input.drafts = DraftHandling.DELETE;
}
if (input.labels != null) {
checkLabels(revision, input.strictLabels, input.labels);
@@ -248,6 +245,13 @@
"label required to post review on behalf of \"%s\"",
in.onBehalfOf));
}
+ if (in.drafts == null) {
+ in.drafts = DraftHandling.KEEP;
+ }
+ if (in.drafts != DraftHandling.KEEP) {
+ throw new AuthException("not allowed to modify other user's drafts");
+ }
+
ChangeControl caller = rev.getControl();
Iterator<Map.Entry<String, Short>> itr = in.labels.entrySet().iterator();
@@ -279,7 +283,13 @@
in.onBehalfOf));
}
- ChangeControl target = caller.forUser(accounts.parse(in.onBehalfOf));
+ ChangeControl target = caller.forUser(
+ accounts.parseOnBehalfOf(caller.getUser(), in.onBehalfOf));
+ if (!target.getRefControl().isVisible()) {
+ throw new UnprocessableEntityException(String.format(
+ "on_behalf_of account %s cannot see destination ref",
+ target.getUser().getAccountId()));
+ }
return new RevisionResource(changes.parse(target), rev.getPatchSet());
}
@@ -516,14 +526,7 @@
String parent = Url.decode(c.inReplyTo);
Comment e = drafts.remove(Url.decode(c.id));
if (e == null) {
- e = new Comment(
- new Comment.Key(ChangeUtil.messageUUID(ctx.getDb()), path,
- psId.get()),
- user.getAccountId(),
- ctx.getWhen(),
- c.side(),
- c.message,
- serverId);
+ e = commentsUtil.newComment(ctx, path, psId, c.side(), c.message);
} else {
e.writtenOn = ctx.getWhen();
e.side = c.side();
@@ -544,7 +547,7 @@
}
}
- switch (firstNonNull(in.drafts, DraftHandling.DELETE)) {
+ switch (in.drafts) {
case KEEP:
default:
break;
@@ -581,13 +584,11 @@
for (Map.Entry<String, List<RobotCommentInput>> ent : in.robotComments.entrySet()) {
String path = ent.getKey();
for (RobotCommentInput c : ent.getValue()) {
- RobotComment e = new RobotComment(
- new Comment.Key(ChangeUtil.messageUUID(ctx.getDb()), path,
- psId.get()),
- user.getAccountId(), ctx.getWhen(), c.side(), c.message, serverId,
- c.robotId, c.robotRunId);
+ RobotComment e = commentsUtil.newRobotComment(
+ ctx, path, psId, c.side(), c.message, c.robotId, c.robotRunId);
e.parentUuid = Url.decode(c.inReplyTo);
e.url = c.url;
+ e.properties = c.properties;
e.setLineNbrAndRange(c.line, c.range);
e.tag = in.tag;
setCommentRevId(e, patchListCache, ctx.getChange(), ps);
@@ -650,6 +651,10 @@
Comment c, PatchSet ps) throws OrmException {
c.writtenOn = ctx.getWhen();
c.tag = in.tag;
+ // Draft may have been created by a different real user; copy the current
+ // real user. (Only applies to X-Gerrit-RunAs, since modifying drafts via
+ // on_behalf_of is not allowed.)
+ ctx.getUser().updateRealAccountId(c::setRealAuthor);
setCommentRevId(c, patchListCache, ctx.getChange(), checkNotNull(ps));
return c;
}
@@ -770,6 +775,7 @@
c.setValue(ent.getValue());
c.setGranted(ctx.getWhen());
c.setTag(in.tag);
+ ctx.getUser().updateRealAccountId(c::setRealAccountId);
ups.add(c);
addLabelDelta(normName, c.getValue());
oldApprovals.put(normName, previous.get(normName));
@@ -780,11 +786,8 @@
oldApprovals.put(normName, null);
approvals.put(normName, c.getValue());
} else if (c == null) {
- c = new PatchSetApproval(new PatchSetApproval.Key(
- psId,
- user.getAccountId(),
- lt.getLabelId()),
- ent.getValue(), ctx.getWhen());
+ c = ApprovalsUtil.newApproval(
+ psId, user, lt.getLabelId(), ent.getValue(), ctx.getWhen());
c.setTag(in.tag);
c.setGranted(ctx.getWhen());
ups.add(c);
@@ -822,12 +825,10 @@
if (del.isEmpty()) {
// If no existing label is being set to 0, hack in the caller
// as a reviewer by picking the first server-wide LabelType.
- PatchSetApproval c = new PatchSetApproval(new PatchSetApproval.Key(
- psId,
- user.getAccountId(),
- ctx.getControl().getLabelTypes().getLabelTypes().get(0)
- .getLabelId()),
- (short) 0, ctx.getWhen());
+ LabelId labelId = ctx.getControl().getLabelTypes().getLabelTypes()
+ .get(0).getLabelId();
+ PatchSetApproval c = ApprovalsUtil.newApproval(
+ psId, user, labelId, 0, ctx.getWhen());
c.setTag(in.tag);
c.setGranted(ctx.getWhen());
ups.add(c);
@@ -886,17 +887,10 @@
return false;
}
- message = new ChangeMessage(
- new ChangeMessage.Key(
- psId.getParentKey(), ChangeUtil.messageUUID(ctx.getDb())),
- user.getAccountId(),
- ctx.getWhen(),
- psId);
+ message = ChangeMessagesUtil.newMessage(
+ ctx.getDb(), psId, user, ctx.getWhen(),
+ "Patch Set " + psId.get() + ":" + buf);
message.setTag(in.tag);
- message.setMessage(String.format(
- "Patch Set %d:%s",
- psId.get(),
- buf.toString()));
cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), message);
return true;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index 287ee7f..ee771f1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -41,6 +41,7 @@
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.account.AccountsCollection;
import com.google.gerrit.server.account.GroupMembers;
@@ -99,6 +100,7 @@
private final ReviewerJson json;
private final ReviewerAdded reviewerAdded;
private final NotesMigration migration;
+ private final AccountCache accountCache;
@Inject
PostReviewers(AccountsCollection accounts,
@@ -116,7 +118,8 @@
@GerritServerConfig Config cfg,
ReviewerJson json,
ReviewerAdded reviewerAdded,
- NotesMigration migration) {
+ NotesMigration migration,
+ AccountCache accountCache) {
this.accounts = accounts;
this.reviewerFactory = reviewerFactory;
this.approvalsUtil = approvalsUtil;
@@ -133,6 +136,7 @@
this.json = json;
this.reviewerAdded = reviewerAdded;
this.migration = migration;
+ this.accountCache = accountCache;
}
@Override
@@ -362,8 +366,8 @@
}
emailReviewers(rsrc.getChange(), addedReviewers, addedCCs, notify);
if (!addedReviewers.isEmpty()) {
- List<Account.Id> reviewers =
- Lists.transform(addedReviewers, PatchSetApproval::getAccountId);
+ List<Account> reviewers = Lists.transform(addedReviewers,
+ psa -> accountCache.get(psa.getAccountId()).getAccount());
reviewerAdded.fire(rsrc.getChange(), patchSet, reviewers,
ctx.getAccount(), ctx.getWhen());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index 9d997f6..1dcbf85 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -34,7 +34,7 @@
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountResolver;
@@ -223,7 +223,7 @@
@Override
public void postUpdate(Context ctx) throws OrmException {
- draftPublished.fire(change, patchSet, ctx.getAccountId(),
+ draftPublished.fire(change, patchSet, ctx.getAccount(),
ctx.getWhen());
if (patchSet.isDraft() && change.getStatus() == Change.Status.DRAFT) {
// Skip emails if the patch set is still a draft.
@@ -252,15 +252,12 @@
private void sendReplacePatchSet(Context ctx)
throws EmailException, OrmException {
- Account.Id accountId = ctx.getAccountId();
- ChangeMessage msg =
- new ChangeMessage(new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())), accountId,
- ctx.getWhen(), psId);
- msg.setMessage("Uploaded patch set " + psId.get() + ".");
+ ChangeMessage msg = ChangeMessagesUtil.newMessage(
+ ctx.getDb(), psId, ctx.getUser(), ctx.getWhen(),
+ "Uploaded patch set " + psId.get() + ".");
ReplacePatchSetSender cm =
replacePatchSetFactory.create(ctx.getProject(), change.getId());
- cm.setFrom(accountId);
+ cm.setFrom(ctx.getAccountId());
cm.setPatchSet(patchSet, patchSetInfo);
cm.setChangeMessage(msg.getMessage(), ctx.getWhen());
cm.addReviewers(recipients.getReviewers());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
index 4298937..5002436 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
@@ -14,12 +14,15 @@
package com.google.gerrit.server.change;
+import com.google.common.base.Strings;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.AssigneeInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
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.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -59,10 +62,17 @@
@Override
public Response<AccountInfo> apply(ChangeResource rsrc, AssigneeInput input)
throws RestApiException, UpdateException, OrmException, IOException {
+ if (!rsrc.getControl().canEditAssignee()) {
+ throw new AuthException("Changing Assignee not permitted");
+ }
+ if (Strings.isNullOrEmpty(input.assignee)) {
+ throw new BadRequestException("missing assignee field");
+ }
+
try (BatchUpdate bu = batchUpdateFactory.create(db.get(),
rsrc.getChange().getProject(), rsrc.getControl().getUser(),
TimeUtil.nowTs())) {
- SetAssigneeOp op = assigneeFactory.create(input);
+ SetAssigneeOp op = assigneeFactory.create(input.assignee);
bu.addOp(rsrc.getId(), op);
PostReviewers.Addition reviewersAddition =
@@ -70,7 +80,6 @@
bu.addOp(rsrc.getId(), reviewersAddition.op);
bu.execute();
- reviewersAddition.gatherResults();
return Response.ok(AccountJson.toAccountInfo(op.getNewAssignee()));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
index 0808f95..91ad849 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -121,6 +121,9 @@
}
Comment origComment = maybeComment.get();
comment = new Comment(origComment);
+ // Copy constructor preserved old real author; replace with current real
+ // user.
+ ctx.getUser().updateRealAccountId(comment::setRealAuthor);
PatchSet.Id psId =
new PatchSet.Id(ctx.getChange().getId(), origComment.key.patchSetId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
index 31ae892..c0b3517 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -26,7 +26,6 @@
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.change.PutTopic.Input;
import com.google.gerrit.server.extensions.events.TopicEdited;
import com.google.gerrit.server.git.BatchUpdate;
@@ -115,13 +114,7 @@
change.setTopic(Strings.emptyToNull(newTopicName));
update.setTopic(change.getTopic());
- ChangeMessage cmsg = new ChangeMessage(
- new ChangeMessage.Key(
- change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(),
- change.currentPatchSetId());
- cmsg.setMessage(summary);
+ ChangeMessage cmsg = ChangeMessagesUtil.newMessage(ctx, summary);
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
return true;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
index 9c4c6d9..f317ef2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -29,7 +29,6 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.extensions.events.ChangeRestored;
import com.google.gerrit.server.git.BatchUpdate;
@@ -131,16 +130,7 @@
msg.append("\n\n");
msg.append(input.message.trim());
}
-
- ChangeMessage message = new ChangeMessage(
- new ChangeMessage.Key(
- change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(),
- ctx.getWhen(),
- change.currentPatchSetId());
- message.setMessage(msg.toString());
- return message;
+ return ChangeMessagesUtil.newMessage(ctx, msg.toString());
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index 3ca496a..c42b257 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -34,7 +34,6 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.PatchSetUtil;
@@ -274,14 +273,8 @@
public boolean updateChange(ChangeContext ctx) throws Exception {
Change change = ctx.getChange();
PatchSet.Id patchSetId = change.currentPatchSetId();
- ChangeMessage changeMessage = new ChangeMessage(
- new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(db.get())),
- ctx.getAccountId(), ctx.getWhen(), patchSetId);
- StringBuilder msgBuf = new StringBuilder();
- msgBuf.append("Created a revert of this change as ")
- .append("I").append(computedChangeId.name());
- changeMessage.setMessage(msgBuf.toString());
+ ChangeMessage changeMessage = ChangeMessagesUtil.newMessage(ctx,
+ "Created a revert of this change as I" + computedChangeId.name());
cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(patchSetId),
changeMessage);
return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestion.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestion.java
new file mode 100644
index 0000000..6affd9f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerSuggestion.java
@@ -0,0 +1,45 @@
+// 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 com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+
+import java.util.Set;
+
+/**
+ * Listener to provide reviewer suggestions.
+ * <p>
+ * Invoked by Gerrit a user who is searching for a reviewer to add to a change.
+ */
+@ExtensionPoint
+public interface ReviewerSuggestion {
+ /**
+ * Reviewer suggestion.
+ *
+ * @param project The name key of the project the suggestion is for.
+ * @param changeId The changeId that the suggestion is for. Can be an {@code null}.
+ * @param query The query as typed by the user. Can be an {@code null}.
+ * @param candidates A set of candidates for the ranking. Can be empty.
+ * @return Set of suggested reviewers as a tuple of account id and score.
+ * The account ids listed here don't have to be a part of candidates.
+ */
+ Set<SuggestedReviewer> suggestReviewers(Project.NameKey project,
+ @Nullable Change.Id changeId, @Nullable String query,
+ Set<Account.Id> candidates);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java
index a8ca147..7ef72ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -14,18 +14,18 @@
package com.google.gerrit.server.change;
+import static com.google.common.base.Preconditions.checkNotNull;
+
import com.google.common.base.Optional;
-import com.google.gerrit.extensions.api.changes.AssigneeInput;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.AuthException;
-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.UnprocessableEntityException;
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.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountInfoCacheFactory;
import com.google.gerrit.server.account.AccountsCollection;
@@ -42,14 +42,14 @@
public class SetAssigneeOp extends BatchUpdate.Op {
public interface Factory {
- SetAssigneeOp create(AssigneeInput input);
+ SetAssigneeOp create(String assignee);
}
private final AccountsCollection accounts;
private final ChangeMessagesUtil cmUtil;
private final AccountInfoCacheFactory.Factory accountInfosFactory;
private final DynamicSet<AssigneeValidationListener> validationListeners;
- private final AssigneeInput input;
+ private final String assignee;
private final String anonymousCowardName;
private final AssigneeChanged assigneeChanged;
@@ -64,37 +64,28 @@
DynamicSet<AssigneeValidationListener> validationListeners,
AssigneeChanged assigneeChanged,
@AnonymousCowardName String anonymousCowardName,
- @Assisted AssigneeInput input) {
+ @Assisted String assignee) {
this.accounts = accounts;
this.cmUtil = cmUtil;
this.accountInfosFactory = accountInfosFactory;
this.validationListeners = validationListeners;
this.assigneeChanged = assigneeChanged;
this.anonymousCowardName = anonymousCowardName;
- this.input = input;
+ this.assignee = checkNotNull(assignee);
}
@Override
public boolean updateChange(BatchUpdate.ChangeContext ctx)
throws OrmException, RestApiException {
- if (!ctx.getControl().canEditAssignee()) {
- throw new AuthException("Changing Assignee not permitted");
- }
change = ctx.getChange();
ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
Optional<Account.Id> oldAssigneeId =
Optional.fromNullable(change.getAssignee());
- if (input.assignee == null) {
- if (oldAssigneeId.isPresent()) {
- throw new BadRequestException("Cannot set Assignee to empty");
- }
- return false;
- }
oldAssignee = null;
if (oldAssigneeId.isPresent()) {
oldAssignee = accountInfosFactory.create().get(oldAssigneeId.get());
}
- IdentifiedUser newAssigneeUser = accounts.parse(input.assignee);
+ IdentifiedUser newAssigneeUser = accounts.parse(assignee);
if (oldAssigneeId.isPresent() &&
oldAssigneeId.get().equals(newAssigneeUser.getAccountId())) {
newAssignee = oldAssignee;
@@ -102,20 +93,20 @@
}
if (!newAssigneeUser.getAccount().isActive()) {
throw new UnprocessableEntityException(String.format(
- "Account of %s is not active", input.assignee));
+ "Account of %s is not active", assignee));
}
if (!ctx.getControl().forUser(newAssigneeUser).isRefVisible()) {
throw new AuthException(String.format(
"Change %s is not visible to %s.",
change.getChangeId(),
- input.assignee));
+ assignee));
}
try {
for (AssigneeValidationListener validator : validationListeners) {
validator.validateAssignee(change, newAssigneeUser.getAccount());
}
} catch (ValidationException e) {
- throw new BadRequestException(e.getMessage());
+ throw new ResourceConflictException(e.getMessage());
}
// notedb
update.setAssignee(newAssigneeUser.getAccountId());
@@ -139,13 +130,7 @@
msg.append(" to: ");
msg.append(newAssignee.getName(anonymousCowardName));
}
- ChangeMessage cmsg = new ChangeMessage(
- new ChangeMessage.Key(
- change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(),
- change.currentPatchSetId());
- cmsg.setMessage(msg.toString());
+ ChangeMessage cmsg = ChangeMessagesUtil.newMessage(ctx, msg.toString());
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
index 34c611b..cf6d307 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -28,7 +28,6 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.extensions.events.HashtagsEdited;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -129,18 +128,12 @@
return true;
}
- private void addMessage(Context ctx, ChangeUpdate update)
+ private void addMessage(ChangeContext ctx, ChangeUpdate update)
throws OrmException {
StringBuilder msg = new StringBuilder();
appendHashtagMessage(msg, "added", toAdd);
appendHashtagMessage(msg, "removed", toRemove);
- ChangeMessage cmsg = new ChangeMessage(
- new ChangeMessage.Key(
- change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(),
- change.currentPatchSetId());
- cmsg.setMessage(msg.toString());
+ ChangeMessage cmsg = ChangeMessagesUtil.newMessage(ctx, msg.toString());
cmUtil.addChangeMessage(ctx.getDb(), update, cmsg);
}
@@ -166,7 +159,7 @@
@Override
public void postUpdate(Context ctx) throws OrmException {
if (updated() && fireEvent) {
- hashtagsEdited.fire(change, ctx.getAccountId(), updatedHashtags,
+ hashtagsEdited.fire(change, ctx.getAccount(), updatedHashtags,
toAdd, toRemove, ctx.getWhen());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index b8443be..e80e758 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -485,16 +485,12 @@
if (!caller.canSubmitAs()) {
throw new AuthException("submit on behalf of not permitted");
}
- IdentifiedUser targetUser = accounts.parseId(in.onBehalfOf);
- if (targetUser == null) {
- throw new UnprocessableEntityException(String.format(
- "Account Not Found: %s", in.onBehalfOf));
- }
- ChangeControl target = caller.forUser(targetUser);
+ ChangeControl target = caller.forUser(
+ accounts.parseOnBehalfOf(caller.getUser(), in.onBehalfOf));
if (!target.getRefControl().isVisible()) {
throw new UnprocessableEntityException(String.format(
"on_behalf_of account %s cannot see destination ref",
- targetUser.getAccountId()));
+ target.getUser().getAccountId()));
}
return new RevisionResource(changes.parse(target), rsrc.getPatchSet());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
index a1d53e0..131513b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
@@ -54,7 +54,7 @@
@Override
public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
throws BadRequestException, OrmException, IOException {
- return reviewersUtil.suggestReviewers(this,
+ return reviewersUtil.suggestReviewers(rsrc.getNotes(), this,
rsrc.getControl().getProjectControl(), getVisibility(rsrc), excludeGroups);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
index f159c69..2af1f6b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestReviewers.java
@@ -33,7 +33,6 @@
protected final ReviewersUtil reviewersUtil;
private final boolean suggestAccounts;
- private final int suggestFrom;
private final int maxAllowed;
private final int maxAllowedWithoutConfirmation;
protected int limit;
@@ -62,10 +61,6 @@
return suggestAccounts;
}
- public int getSuggestFrom() {
- return suggestFrom;
- }
-
public int getLimit() {
return limit;
}
@@ -98,7 +93,6 @@
this.suggestAccounts = (av != AccountVisibility.NONE);
}
- this.suggestFrom = cfg.getInt("suggest", null, "from", 0);
this.maxAllowed = cfg.getInt("addreviewer", "maxAllowed",
PostReviewers.DEFAULT_MAX_REVIEWERS);
this.maxAllowedWithoutConfirmation = cfg.getInt(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestedReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestedReviewer.java
new file mode 100644
index 0000000..353bf3b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestedReviewer.java
@@ -0,0 +1,22 @@
+// 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 com.google.gerrit.reviewdb.client.Account;
+
+public class SuggestedReviewer {
+
+ public Account.Id account;
+ public double score;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index fecb156..51dbff2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -100,6 +100,7 @@
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.change.ChangeKindCacheImpl;
import com.google.gerrit.server.change.MergeabilityCacheImpl;
+import com.google.gerrit.server.change.ReviewerSuggestion;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.events.EventsMetrics;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -352,6 +353,7 @@
DynamicMap.mapOf(binder(), DownloadScheme.class);
DynamicMap.mapOf(binder(), DownloadCommand.class);
DynamicMap.mapOf(binder(), CloneCommand.class);
+ DynamicMap.mapOf(binder(), ReviewerSuggestion.class);
DynamicSet.setOf(binder(), ExternalIncludedIn.class);
DynamicMap.mapOf(binder(), ProjectConfigEntry.class);
DynamicSet.setOf(binder(), PatchSetWebLink.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 85098d4d..c867d26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -39,7 +39,6 @@
import com.google.gerrit.extensions.events.TopicEditedListener;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
@@ -56,6 +55,7 @@
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -90,7 +90,7 @@
private static final Logger log =
LoggerFactory.getLogger(StreamEventsApiListener.class);
- public static class Module extends LifecycleModule {
+ public static class Module extends AbstractModule {
@Override
protected void configure() {
DynamicSet.bind(binder(), AssigneeChangedListener.class)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AgreementSignup.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AgreementSignup.java
index 018d408..c910a7a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AgreementSignup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AgreementSignup.java
@@ -20,13 +20,7 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.inject.Inject;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
public class AgreementSignup {
- private static final Logger log =
- LoggerFactory.getLogger(AgreementSignup.class);
-
private final DynamicSet<AgreementSignupListener> listeners;
private final EventUtil util;
@@ -46,7 +40,7 @@
try {
l.onAgreementSignup(event);
} catch (Exception e) {
- log.warn("Error in event listener", e);
+ util.logEventListenerError(this, l, e);
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
index 2234556..53d837f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
@@ -43,31 +43,24 @@
this.util = util;
}
- public void fire(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee,
- Timestamp when) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(change, editor, oldAssignee, when);
- for (AssigneeChangedListener l : listeners) {
- try {
- l.onAssigneeChanged(event);
- } catch (Exception e) {
- log.warn("Error in event listener", e);
- }
- }
- }
-
public void fire(Change change, Account account, Account oldAssignee,
Timestamp when) {
if (!listeners.iterator().hasNext()) {
return;
}
try {
- fire(util.changeInfo(change),
+ Event event = new Event(
+ util.changeInfo(change),
util.accountInfo(account),
util.accountInfo(oldAssignee),
when);
+ for (AssigneeChangedListener l : listeners) {
+ try {
+ l.onAssigneeChanged(event);
+ } catch (Exception e) {
+ util.logEventListenerError(event, l, e);
+ }
+ }
} catch (OrmException e) {
log.error("Couldn't fire event", e);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
index c76e76b..5a7aec2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
@@ -48,33 +48,24 @@
this.util = util;
}
- public void fire(ChangeInfo change, RevisionInfo revision,
- AccountInfo abandoner, String reason, Timestamp when,
- NotifyHandling notifyHandling) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(change, revision, abandoner, reason, when,
- notifyHandling);
- for (ChangeAbandonedListener l : listeners) {
- try {
- l.onChangeAbandoned(event);
- } catch (Exception e) {
- log.warn("Error in event listener", e);
- }
- }
- }
-
public void fire(Change change, PatchSet ps, Account abandoner, String reason,
Timestamp when, NotifyHandling notifyHandling) {
if (!listeners.iterator().hasNext()) {
return;
}
try {
- fire(util.changeInfo(change),
+ Event event = new Event(
+ util.changeInfo(change),
util.revisionInfo(change.getProject(), ps),
util.accountInfo(abandoner),
reason, when, notifyHandling);
+ for (ChangeAbandonedListener l : listeners) {
+ try {
+ l.onChangeAbandoned(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, l, e);
+ }
+ }
} catch (PatchListNotAvailableException | GpgException | IOException
| OrmException e) {
log.error("Couldn't fire event", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
index 378f2b7..8b4a6a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
@@ -48,31 +48,24 @@
this.util = util;
}
- public void fire(ChangeInfo change, RevisionInfo revision,
- AccountInfo merger, String newRevisionId, Timestamp when) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(change, revision, merger, newRevisionId, when);
- for (ChangeMergedListener l : listeners) {
- try {
- l.onChangeMerged(event);
- } catch (Exception e) {
- log.warn("Error in event listener", e);
- }
- }
- }
-
public void fire(Change change, PatchSet ps, Account merger,
String newRevisionId, Timestamp when) {
if (!listeners.iterator().hasNext()) {
return;
}
try {
- fire(util.changeInfo(change),
+ Event event = new Event(
+ util.changeInfo(change),
util.revisionInfo(change.getProject(), ps),
util.accountInfo(merger),
newRevisionId, when);
+ for (ChangeMergedListener l : listeners) {
+ try {
+ l.onChangeMerged(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, l, e);
+ }
+ }
} catch (PatchListNotAvailableException | GpgException | IOException
| OrmException e) {
log.error("Couldn't fire event", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
index 05e0d21..1d2682a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
@@ -48,31 +48,24 @@
this.util = util;
}
- public void fire(ChangeInfo change, RevisionInfo revision,
- AccountInfo restorer, String reason, Timestamp when) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(change, revision, restorer, reason, when);
- for (ChangeRestoredListener l : listeners) {
- try {
- l.onChangeRestored(event);
- } catch (Exception e) {
- log.warn("Error in event listener", e);
- }
- }
- }
-
public void fire(Change change, PatchSet ps, Account restorer, String reason,
Timestamp when) {
if (!listeners.iterator().hasNext()) {
return;
}
try {
- fire(util.changeInfo(change),
+ Event event = new Event(
+ util.changeInfo(change),
util.revisionInfo(change.getProject(), ps),
util.accountInfo(restorer),
reason, when);
+ for (ChangeRestoredListener l : listeners) {
+ try {
+ l.onChangeRestored(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, l, e);
+ }
+ }
} catch (PatchListNotAvailableException | GpgException | IOException
| OrmException e) {
log.error("Couldn't fire event", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeReverted.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
index f95236d..d963a47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
@@ -46,27 +46,20 @@
return;
}
try {
- fire(util.changeInfo(change), util.changeInfo(revertChange), when);
+ Event event = new Event(
+ util.changeInfo(change), util.changeInfo(revertChange), when);
+ for (ChangeRevertedListener l : listeners) {
+ try {
+ l.onChangeReverted(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, l, e);
+ }
+ }
} catch (OrmException e) {
log.error("Couldn't fire event", e);
}
}
- public void fire (ChangeInfo change, ChangeInfo revertChange,
- Timestamp when) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(change, revertChange, when);
- for (ChangeRevertedListener l : listeners) {
- try {
- l.onChangeReverted(event);
- } catch (Exception e) {
- log.warn("Error in event listener", e);
- }
- }
- }
-
private static class Event extends AbstractChangeEvent
implements ChangeRevertedListener.Event {
private final ChangeInfo revertChange;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
index 8e27ce9..f1bb50a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
@@ -50,23 +50,6 @@
this.util = util;
}
- public void fire(ChangeInfo change, RevisionInfo revision, AccountInfo author,
- String comment, Map<String, ApprovalInfo> approvals,
- Map<String, ApprovalInfo> oldApprovals, Timestamp when) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(
- change, revision, author, comment, approvals, oldApprovals, when);
- for (CommentAddedListener l : listeners) {
- try {
- l.onCommentAdded(event);
- } catch (Exception e) {
- log.warn("Error in event listener", e);
- }
- }
- }
-
public void fire(Change change, PatchSet ps, Account author,
String comment, Map<String, Short> approvals,
Map<String, Short> oldApprovals, Timestamp when) {
@@ -74,13 +57,20 @@
return;
}
try {
- fire(util.changeInfo(change),
+ Event event = new Event(util.changeInfo(change),
util.revisionInfo(change.getProject(), ps),
util.accountInfo(author),
comment,
util.approvals(author, approvals, when),
util.approvals(author, oldApprovals, when),
when);
+ for (CommentAddedListener l : listeners) {
+ try {
+ l.onCommentAdded(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, l, e);
+ }
+ }
} catch (PatchListNotAvailableException | GpgException | IOException
| OrmException e) {
log.error("Couldn't fire event", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
index 6b8ce3d..4f6d298 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
@@ -48,28 +48,21 @@
this.util = util;
}
- public void fire(ChangeInfo change, RevisionInfo revision,
- AccountInfo publisher, Timestamp when) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(change, revision, publisher, when);
- for (DraftPublishedListener l : listeners) {
- try {
- l.onDraftPublished(event);
- } catch (Exception e) {
- log.warn("Error in event listener", e);
- }
- }
- }
-
- public void fire(Change change, PatchSet patchSet, Account.Id accountId,
+ public void fire(Change change, PatchSet patchSet, Account accountId,
Timestamp when) {
try {
- fire(util.changeInfo(change),
+ Event event = new Event(
+ util.changeInfo(change),
util.revisionInfo(change.getProject(), patchSet),
util.accountInfo(accountId),
when);
+ for (DraftPublishedListener l : listeners) {
+ try {
+ l.onDraftPublished(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, l, e);
+ }
+ }
} catch (PatchListNotAvailableException | GpgException | IOException
| OrmException e) {
log.error("Couldn't fire event", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
index 1c8782b..682d9ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -25,7 +25,6 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GpgException;
-import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountJson;
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -35,6 +34,9 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.io.IOException;
import java.sql.Timestamp;
import java.util.EnumSet;
@@ -42,22 +44,21 @@
import java.util.Map;
public class EventUtil {
+ private static final Logger log = LoggerFactory.getLogger(EventUtil.class);
private final ChangeData.Factory changeDataFactory;
private final Provider<ReviewDb> db;
private final ChangeJson changeJson;
- private final AccountCache accountCache;
@Inject
EventUtil(ChangeJson.Factory changeJsonFactory,
ChangeData.Factory changeDataFactory,
- Provider<ReviewDb> db,
- AccountCache accountCache) {
+ Provider<ReviewDb> db) {
this.changeDataFactory = changeDataFactory;
this.db = db;
- this.changeJson = changeJsonFactory.create(
- EnumSet.allOf(ListChangesOption.class));
- this.accountCache = accountCache;
+ EnumSet<ListChangesOption> opts = EnumSet.allOf(ListChangesOption.class);
+ opts.remove(ListChangesOption.CHECK);
+ this.changeJson = changeJsonFactory.create(opts);
}
public ChangeInfo changeInfo(Change change) throws OrmException {
@@ -86,10 +87,6 @@
return AccountJson.toAccountInfo(a);
}
- public AccountInfo accountInfo(Account.Id accountId) {
- return accountInfo(accountCache.get(accountId).getAccount());
- }
-
public Map<String, ApprovalInfo> approvals(Account a,
Map<String, Short> approvals, Timestamp ts) {
Map<String, ApprovalInfo> result = new HashMap<>();
@@ -100,4 +97,17 @@
}
return result;
}
+
+ public void logEventListenerError(Object event, Object listener,
+ Exception error) {
+ if (log.isDebugEnabled()) {
+ log.debug(String.format(
+ "Error in event listener %s for event %s",
+ listener.getClass().getName(), event.getClass().getName()), error);
+ } else {
+ log.warn("Error in listener {} for event {}: {}",
+ listener.getClass().getName(), event.getClass().getName(),
+ error.getMessage());
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 386bcea..381dced 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -26,13 +26,8 @@
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.transport.ReceiveCommand;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
public class GitReferenceUpdated {
- private static final Logger log = LoggerFactory
- .getLogger(GitReferenceUpdated.class);
-
public static final GitReferenceUpdated DISABLED = new GitReferenceUpdated() {
@Override
public void fire(Project.NameKey project, RefUpdate refUpdate,
@@ -40,17 +35,9 @@
@Override
public void fire(Project.NameKey project, RefUpdate refUpdate,
- ReceiveCommand.Type type, Account.Id updater) {}
-
- @Override
- public void fire(Project.NameKey project, RefUpdate refUpdate,
Account updater) {}
@Override
- public void fire(Project.NameKey project, RefUpdate refUpdate,
- AccountInfo updater) {}
-
- @Override
public void fire(Project.NameKey project, String ref, ObjectId oldObjectId,
ObjectId newObjectId, Account updater) {}
@@ -60,10 +47,10 @@
@Override
public void fire(Project.NameKey project, BatchRefUpdate batchRefUpdate,
- Account.Id updater) {}
+ Account updater) {}
};
- private final Iterable<GitReferenceUpdatedListener> listeners;
+ private final DynamicSet<GitReferenceUpdatedListener> listeners;
private final EventUtil util;
@Inject
@@ -85,24 +72,12 @@
}
public void fire(Project.NameKey project, RefUpdate refUpdate,
- ReceiveCommand.Type type, Account.Id updater) {
- fire(project, refUpdate.getName(), refUpdate.getOldObjectId(),
- refUpdate.getNewObjectId(), type, util.accountInfo(updater));
- }
-
- public void fire(Project.NameKey project, RefUpdate refUpdate,
Account updater) {
fire(project, refUpdate.getName(), refUpdate.getOldObjectId(),
refUpdate.getNewObjectId(), ReceiveCommand.Type.UPDATE,
util.accountInfo(updater));
}
- public void fire(Project.NameKey project, RefUpdate refUpdate,
- AccountInfo updater) {
- fire(project, refUpdate.getName(), refUpdate.getOldObjectId(),
- refUpdate.getNewObjectId(), ReceiveCommand.Type.UPDATE, updater);
- }
-
public void fire(Project.NameKey project, String ref, ObjectId oldObjectId,
ObjectId newObjectId, Account updater) {
fire(project, ref, oldObjectId, newObjectId, ReceiveCommand.Type.UPDATE,
@@ -115,23 +90,22 @@
}
public void fire(Project.NameKey project, BatchRefUpdate batchRefUpdate,
- Account.Id updater) {
+ Account updater) {
if (!listeners.iterator().hasNext()) {
return;
}
for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
if (cmd.getResult() == ReceiveCommand.Result.OK) {
- fire(project, cmd, util.accountInfo(updater));
+ fire(project,
+ cmd.getRefName(),
+ cmd.getOldId(),
+ cmd.getNewId(),
+ cmd.getType(),
+ util.accountInfo(updater));
}
}
}
- private void fire(Project.NameKey project, ReceiveCommand cmd,
- AccountInfo updater) {
- fire(project, cmd.getRefName(), cmd.getOldId(), cmd.getNewId(), cmd.getType(),
- updater);
- }
-
private void fire(Project.NameKey project, String ref, ObjectId oldObjectId,
ObjectId newObjectId, ReceiveCommand.Type type, AccountInfo updater) {
if (!listeners.iterator().hasNext()) {
@@ -144,7 +118,7 @@
try {
l.onGitReferenceUpdated(event);
} catch (Exception e) {
- log.warn("Error in event listener", e);
+ util.logEventListenerError(this, l, e);
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
index f18b963..233a89e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
@@ -20,7 +20,7 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.events.HashtagsEditedListener;
import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -46,33 +46,25 @@
this.util = util;
}
- public void fire(ChangeInfo change, AccountInfo editor,
- Collection<String> hashtags, Collection<String> added,
- Collection<String> removed, Timestamp when) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(change, editor, hashtags, added, removed, when);
- for (HashtagsEditedListener l : listeners) {
- try {
- l.onHashtagsEdited(event);
- } catch (Exception e) {
- log.warn("Error in event listener", e);
- }
- }
- }
-
- public void fire(Change change, Id accountId,
+ public void fire(Change change, Account editor,
ImmutableSortedSet<String> hashtags, Set<String> added,
Set<String> removed, Timestamp when) {
if (!listeners.iterator().hasNext()) {
return;
}
try {
- fire(util.changeInfo(change),
- util.accountInfo(accountId),
+ Event event = new Event(
+ util.changeInfo(change),
+ util.accountInfo(editor),
hashtags, added, removed,
when);
+ for (HashtagsEditedListener l : listeners) {
+ try {
+ l.onHashtagsEdited(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, l, e);
+ }
+ }
} catch (OrmException e) {
log.error("Couldn't fire event", e);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
index 9dbbeef..8860a42 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
@@ -50,33 +50,26 @@
this.util = util;
}
- public void fire(ChangeInfo change, RevisionInfo revision,
- List<AccountInfo> reviewers, AccountInfo adder, Timestamp when) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(change, revision, reviewers, adder, when);
- for (ReviewerAddedListener l : listeners) {
- try {
- l.onReviewersAdded(event);
- } catch (Exception e) {
- log.warn("Error in event listener, e");
- }
- }
- }
-
- public void fire(Change change, PatchSet patchSet, List<Account.Id> reviewers,
+ public void fire(Change change, PatchSet patchSet, List<Account> reviewers,
Account adder, Timestamp when) {
if (!listeners.iterator().hasNext() || reviewers.isEmpty()) {
return;
}
try {
- fire(util.changeInfo(change),
+ Event event = new Event(
+ util.changeInfo(change),
util.revisionInfo(change.getProject(), patchSet),
Lists.transform(reviewers, util::accountInfo),
util.accountInfo(adder),
when);
+ for (ReviewerAddedListener l : listeners) {
+ try {
+ l.onReviewersAdded(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, l, e);
+ }
+ }
} catch (PatchListNotAvailableException | GpgException | IOException
| OrmException e) {
log.error("Couldn't fire event", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
index b519c46..4bc4764 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
@@ -50,25 +50,6 @@
this.util = util;
}
- public void fire(ChangeInfo change, RevisionInfo revision,
- AccountInfo reviewer, AccountInfo remover, String message,
- Map<String, ApprovalInfo> newApprovals,
- Map<String, ApprovalInfo> oldApprovals, NotifyHandling notify,
- Timestamp when) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(change, revision, reviewer, remover, message,
- newApprovals, oldApprovals, notify, when);
- for (ReviewerDeletedListener listener : listeners) {
- try {
- listener.onReviewerDeleted(event);
- } catch (Exception e) {
- log.warn("Error in event listener", e);
- }
- }
- }
-
public void fire(Change change, PatchSet patchSet, Account reviewer,
Account remover, String message, Map<String, Short> newApprovals,
Map<String, Short> oldApprovals, NotifyHandling notify, Timestamp when) {
@@ -76,7 +57,8 @@
return;
}
try {
- fire(util.changeInfo(change),
+ Event event = new Event(
+ util.changeInfo(change),
util.revisionInfo(change.getProject(), patchSet),
util.accountInfo(reviewer),
util.accountInfo(remover),
@@ -85,6 +67,13 @@
util.approvals(reviewer, oldApprovals, when),
notify,
when);
+ for (ReviewerDeletedListener listener : listeners) {
+ try {
+ listener.onReviewerDeleted(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, listener, e);
+ }
+ }
} catch (PatchListNotAvailableException | GpgException | IOException
| OrmException e) {
log.error("Couldn't fire event", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
index 71bc9ec..7f03c63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -48,31 +48,24 @@
this.util = util;
}
- public void fire(ChangeInfo change, RevisionInfo revision,
- AccountInfo uploader, Timestamp when, NotifyHandling notify) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(change, revision, uploader, when, notify);
- for (RevisionCreatedListener l : listeners) {
- try {
- l.onRevisionCreated(event);
- } catch (Exception e) {
- log.warn("Error in event listener", e);
- }
- }
- }
-
- public void fire(Change change, PatchSet patchSet, Account.Id uploader,
+ public void fire(Change change, PatchSet patchSet, Account uploader,
Timestamp when, NotifyHandling notify) {
if (!listeners.iterator().hasNext()) {
return;
}
try {
- fire(util.changeInfo(change),
+ Event event = new Event(
+ util.changeInfo(change),
util.revisionInfo(change.getProject(), patchSet),
util.accountInfo(uploader),
when, notify);
+ for (RevisionCreatedListener l : listeners) {
+ try {
+ l.onRevisionCreated(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, l, e);
+ }
+ }
} catch ( PatchListNotAvailableException | GpgException | IOException
| OrmException e) {
log.error("Couldn't fire event", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
index 77c1647..2e583a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
@@ -43,31 +43,24 @@
this.util = util;
}
- public void fire(ChangeInfo change, AccountInfo editor, String oldTopic,
- Timestamp when) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(change, editor, oldTopic, when);
- for (TopicEditedListener l : listeners) {
- try {
- l.onTopicEdited(event);
- } catch (Exception e) {
- log.warn("Error in event listener", e);
- }
- }
- }
-
public void fire(Change change, Account account, String oldTopicName,
Timestamp when) {
if (!listeners.iterator().hasNext()) {
return;
}
try {
- fire(util.changeInfo(change),
+ Event event = new Event(
+ util.changeInfo(change),
util.accountInfo(account),
oldTopicName,
when);
+ for (TopicEditedListener l : listeners) {
+ try {
+ l.onTopicEdited(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, l, e);
+ }
+ }
} catch (OrmException e) {
log.error("Couldn't fire event", e);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/VoteDeleted.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
index 7772d9c..e421ea6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
@@ -51,26 +51,6 @@
this.util = util;
}
- public void fire(ChangeInfo change, RevisionInfo revision,
- Map<String, ApprovalInfo> approvals,
- Map<String, ApprovalInfo> oldApprovals,
- NotifyHandling notify, String message,
- AccountInfo remover, Timestamp when) {
- if (!listeners.iterator().hasNext()) {
- return;
- }
- Event event = new Event(
- change, revision, approvals, oldApprovals, notify, message,
- remover, when);
- for (VoteDeletedListener l : listeners) {
- try {
- l.onVoteDeleted(event);
- } catch (Exception e) {
- log.warn("Error in event listener", e);
- }
- }
- }
-
public void fire(Change change, PatchSet ps,
Map<String, Short> approvals,
Map<String, Short> oldApprovals,
@@ -80,12 +60,20 @@
return;
}
try {
- fire(util.changeInfo(change),
+ Event event = new Event(
+ util.changeInfo(change),
util.revisionInfo(change.getProject(), ps),
util.approvals(remover, approvals, when),
util.approvals(remover, oldApprovals, when),
notify, message,
util.accountInfo(remover), when);
+ for (VoteDeletedListener l : listeners) {
+ try {
+ l.onVoteDeleted(event);
+ } catch (Exception e) {
+ util.logEventListenerError(this, l, e);
+ }
+ }
} catch (PatchListNotAvailableException | GpgException | IOException
| OrmException e) {
log.error("Couldn't fire event", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index dc1798a..49221c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -444,7 +444,9 @@
u.gitRefUpdated.fire(
u.project,
u.batchRefUpdate,
- u.getUser().isIdentifiedUser() ? u.getUser().getAccountId() : null);
+ u.getUser().isIdentifiedUser()
+ ? u.getUser().asIdentifiedUser().getAccount()
+ : null);
}
}
if (!dryrun) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index a3125a5..0be2a38 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -47,7 +47,6 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -480,7 +479,9 @@
BatchUpdate.execute(orm.batchUpdates(allProjects),
new SubmitStrategyListener(submitInput, strategies, commits),
submissionId, dryrun);
- } catch (UpdateException | SubmoduleException e) {
+ } catch (SubmoduleException e) {
+ throw new IntegrationException(e);
+ } catch (UpdateException e) {
// BatchUpdate may have inadvertently wrapped an IntegrationException
// thrown by some legacy SubmitStrategyOp code that intended the error
// message to be user-visible. Copy the message from the wrapped
@@ -492,8 +493,7 @@
if (e.getCause() instanceof IntegrationException) {
msg = e.getCause().getMessage();
} else {
- msg = "Error submitting change" + (cs.size() != 1 ? "s" : "") + ": \n"
- + e.getMessage();
+ msg = "Error submitting change" + (cs.size() != 1 ? "s" : "");
}
throw new IntegrationException(msg, e);
}
@@ -764,11 +764,10 @@
change.setStatus(Change.Status.ABANDONED);
- ChangeMessage msg = new ChangeMessage(
- new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- null, change.getLastUpdatedOn(), change.currentPatchSetId());
- msg.setMessage("Project was deleted.");
+ ChangeMessage msg = ChangeMessagesUtil.newMessage(
+ ctx.getDb(), change.currentPatchSetId(),
+ internalUserFactory.create(), change.getLastUpdatedOn(),
+ "Project was deleted.");
cmUtil.addChangeMessage(ctx.getDb(),
ctx.getUpdate(change.currentPatchSetId()), msg);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 0667e14..90edfb1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -184,13 +184,13 @@
public CodeReviewCommit createCherryPickFromCommit(Repository repo,
ObjectInserter inserter, RevCommit mergeTip, RevCommit originalCommit,
PersonIdent cherryPickCommitterIdent, String commitMsg,
- CodeReviewRevWalk rw)
+ CodeReviewRevWalk rw, int parentIndex)
throws MissingObjectException, IncorrectObjectTypeException, IOException,
MergeIdenticalTreeException, MergeConflictException {
final ThreeWayMerger m = newThreeWayMerger(repo, inserter);
- m.setBase(originalCommit.getParent(0));
+ m.setBase(originalCommit.getParent(parentIndex));
if (m.merge(mergeTip, originalCommit)) {
ObjectId tree = m.getResultTreeId();
if (tree.equals(mergeTip.getTree())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
index 2ccc849..6da1335 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -22,8 +22,8 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.extensions.events.ChangeMerged;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -149,19 +149,13 @@
}
}
msgBuf.append(".");
- ChangeMessage msg = new ChangeMessage(
- new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(), psId);
- msg.setMessage(msgBuf.toString());
+ ChangeMessage msg = ChangeMessagesUtil.newMessage(
+ ctx.getDb(), psId, ctx.getUser(), ctx.getWhen(), msgBuf.toString());
cmUtil.addChangeMessage(ctx.getDb(), update, msg);
- PatchSetApproval submitter = new PatchSetApproval(
- new PatchSetApproval.Key(
- change.currentPatchSetId(),
- ctx.getAccountId(),
- LabelId.legacySubmit()),
- (short) 1, ctx.getWhen());
+ PatchSetApproval submitter = ApprovalsUtil.newApproval(
+ change.currentPatchSetId(), ctx.getUser(), LabelId.legacySubmit(),
+ 1, ctx.getWhen());
update.putApproval(submitter.getLabel(), submitter.getValue());
ctx.getDb().patchSetApprovals().upsert(
Collections.singleton(submitter));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 414ba5f..d081fe6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -96,29 +96,6 @@
}
}
- synchronized void format(StringBuilder s, boolean first) {
- if (count == 0) {
- return;
- }
-
- if (!first) {
- s.append(',');
- }
- s.append(' ');
-
- if (!Strings.isNullOrEmpty(name)) {
- s.append(name).append(": ");
- }
-
- if (total == UNKNOWN) {
- s.append(count);
- } else {
- s.append(String.format("%d%% (%d/%d)",
- count * 100 / total,
- count, total));
- }
- }
-
/**
* Indicate that this sub-task is finished.
* <p>
@@ -339,9 +316,32 @@
StringBuilder s = new StringBuilder().append("\r").append(taskName)
.append(':');
- int firstLength = s.length();
- for (Task t : tasks) {
- t.format(s, s.length() == firstLength);
+ if (!tasks.isEmpty()) {
+ boolean first = true;
+ for (Task t : tasks) {
+ int count = t.count;
+ if (count == 0) {
+ continue;
+ }
+
+ if (!first) {
+ s.append(',');
+ } else {
+ first = false;
+ }
+
+ s.append(' ');
+ if (!Strings.isNullOrEmpty(t.name)) {
+ s.append(t.name).append(": ");
+ }
+ if (t.total == UNKNOWN) {
+ s.append(count);
+ } else {
+ s.append(String.format("%d%% (%d/%d)",
+ count * 100 / t.total,
+ count, t.total));
+ }
+ }
}
if (spinnerState != NO_SPINNER) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
index d7424c6..99abbc8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -14,7 +14,6 @@
package com.google.gerrit.server.git;
-import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
@@ -262,7 +261,7 @@
throw new IOException("Couldn't update " + notesBranch + ". "
+ result.name());
} else {
- gitRefUpdated.fire(project, refUpdate, (AccountInfo) null);
+ gitRefUpdated.fire(project, refUpdate, null);
break;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 1429079..801f259 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -713,7 +713,7 @@
// no valid UUID for it. Pool the reference anyway so at least
// all rules in the same file share the same GroupReference.
//
- ref = rule.getGroup();
+ ref = resolve(rule.getGroup());
groupsByName.put(ref.getName(), ref);
error(new ValidationError(PROJECT_CONFIG,
"group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME));
@@ -1098,7 +1098,7 @@
boolean needRange = GlobalCapability.hasRange(permission.getName());
List<String> rules = new ArrayList<>();
for (PermissionRule rule : sort(permission.getRules())) {
- GroupReference group = rule.getGroup();
+ GroupReference group = resolve(rule.getGroup());
if (group.getUUID() != null) {
keepGroups.add(group.getUUID());
}
@@ -1143,7 +1143,7 @@
boolean needRange = Permission.hasRange(permission.getName());
List<String> rules = new ArrayList<>();
for (PermissionRule rule : sort(permission.getRules())) {
- GroupReference group = rule.getGroup();
+ GroupReference group = resolve(rule.getGroup());
if (group.getUUID() != null) {
keepGroups.add(group.getUUID());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
index baab110..a9dcd4b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -34,7 +34,6 @@
import com.google.gerrit.server.ApprovalCopier;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeKindCache;
@@ -257,7 +256,7 @@
MailRecipients oldRecipients =
getRecipientsFromReviewers(cd.reviewers());
Iterable<PatchSetApproval> newApprovals =
- approvalsUtil.addApprovals(ctx.getDb(), update,
+ approvalsUtil.addApprovalsForNewPatchSet(ctx.getDb(), update,
projectControl.getLabelTypes(), newPatchSet, ctx.getControl(),
approvals);
approvalCopier.copy(ctx.getDb(), ctx.getControl(), newPatchSet,
@@ -279,11 +278,8 @@
if (!Strings.isNullOrEmpty(reviewMessage)) {
message.append("\n").append(reviewMessage);
}
- msg = new ChangeMessage(
- new ChangeMessage.Key(change.getId(),
- ChangeUtil.messageUUID(ctx.getDb())),
- ctx.getAccountId(), ctx.getWhen(), patchSetId);
- msg.setMessage(message.toString());
+ msg = ChangeMessagesUtil.newMessage(ctx.getDb(), patchSetId, ctx.getUser(),
+ ctx.getWhen(), message.toString());
cmUtil.addChangeMessage(ctx.getDb(), update, msg);
if (mergedByPushOp == null) {
@@ -410,7 +406,7 @@
NotifyHandling notify = magicBranch != null && magicBranch.notify != null
? magicBranch.notify
: NotifyHandling.ALL;
- revisionCreated.fire(change, newPatchSet, ctx.getAccountId(),
+ revisionCreated.fire(change, newPatchSet, ctx.getAccount(),
ctx.getWhen(), notify);
try {
fireCommentAddedEvent(ctx);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 31da05c..c0d96c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -107,7 +107,7 @@
try {
newCommit = args.mergeUtil.createCherryPickFromCommit(
args.repo, args.inserter, args.mergeTip.getCurrentTip(), toMerge,
- committer, cherryPickCmtMsg, args.rw);
+ committer, cherryPickCmtMsg, args.rw, 0);
} catch (MergeConflictException mce) {
// Keep going in the case of a single merge failure; the goal is to
// cherry-pick as many commits as possible.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
index b35de7b..7dacc6f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -33,7 +33,8 @@
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -329,15 +330,9 @@
byKey.put(psa.getKey(), psa);
}
- submitter = new PatchSetApproval(
- new PatchSetApproval.Key(
- psId,
- ctx.getAccountId(),
- LabelId.legacySubmit()),
- (short) 1, ctx.getWhen());
+ submitter = ApprovalsUtil.newApproval(
+ psId, ctx.getUser(), LabelId.legacySubmit(), 1, ctx.getWhen());
byKey.put(submitter.getKey(), submitter);
- submitter.setValue((short) 1);
- submitter.setGranted(ctx.getWhen());
// Flatten out existing approvals for this patch set based upon the current
// permissions. Once the change is closed the approvals are not updated at
@@ -415,7 +410,7 @@
}
private ChangeMessage message(ChangeContext ctx, CodeReviewCommit commit,
- CommitMergeStatus s) {
+ CommitMergeStatus s) throws OrmException {
checkNotNull(s, "CommitMergeStatus may not be null");
String txt = s.getMessage();
if (s == CommitMergeStatus.CLEAN_MERGE) {
@@ -452,19 +447,9 @@
}
private ChangeMessage message(ChangeContext ctx, PatchSet.Id psId,
- String body) {
- checkNotNull(psId);
- String uuid;
- try {
- uuid = ChangeUtil.messageUUID(ctx.getDb());
- } catch (OrmException e) {
- return null;
- }
- ChangeMessage m = new ChangeMessage(
- new ChangeMessage.Key(psId.getParentKey(), uuid),
- ctx.getAccountId(), ctx.getWhen(), psId);
- m.setMessage(body);
- return m;
+ String body) throws OrmException {
+ return ChangeMessagesUtil.newMessage(
+ ctx.getDb(), psId, ctx.getUser(), ctx.getWhen(), body);
}
private void setMerged(ChangeContext ctx, ChangeMessage msg)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index bce114f..dd8bbaa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -18,6 +18,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.Ordering;
import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Change;
@@ -44,7 +45,8 @@
import java.util.List;
import java.util.Set;
-/** Send comments, after the author of them hit used Publish Comments in the UI. */
+/** Send comments, after the author of them hit used Publish Comments in the UI.
+ */
public class CommentSender extends ReplyToChangeSender {
private static final Logger log = LoggerFactory
.getLogger(CommentSender.class);
@@ -183,32 +185,36 @@
out.append(n == range.startLine
? prefix
: Strings.padStart(": ", prefix.length(), ' '));
- try {
- String s = currentFileData.getLine(side, n);
- if (n == range.startLine && n == range.endLine) {
- s = s.substring(
- Math.min(range.startChar, s.length()),
- Math.min(range.endChar, s.length()));
- } else if (n == range.startLine) {
- s = s.substring(Math.min(range.startChar, s.length()));
- } else if (n == range.endLine) {
- s = s.substring(0, Math.min(range.endChar, s.length()));
- }
- out.append(s);
- } catch (Throwable e) {
- // Don't quote the line if we can't safely convert it.
+ String s = getLine(currentFileData, side, n);
+ if (n == range.startLine && n == range.endLine) {
+ s = s.substring(
+ Math.min(range.startChar, s.length()),
+ Math.min(range.endChar, s.length()));
+ } else if (n == range.startLine) {
+ s = s.substring(Math.min(range.startChar, s.length()));
+ } else if (n == range.endLine) {
+ s = s.substring(0, Math.min(range.endChar, s.length()));
}
- out.append('\n');
+ out.append(s).append('\n');
}
appendQuotedParent(out, comment);
out.append(comment.message.trim()).append('\n');
} else {
int lineNbr = comment.lineNbr;
- int maxLines;
+
+ // Initialize maxLines to the known line number.
+ int maxLines = lineNbr;
+
try {
maxLines = currentFileData.getLineCount(side);
- } catch (Throwable e) {
- maxLines = lineNbr;
+ } catch (IOException err) {
+ // The file could not be read, leave the max as is.
+ log.warn(String.format("Failed to read file %s on side %d",
+ comment.key.filename, side), err);
+ } catch (NoSuchEntityException err) {
+ // The file could not be read, leave the max as is.
+ log.warn(String.format("Side %d of file %s didn't exist",
+ side, comment.key.filename), err);
}
final int startLine = Math.max(1, lineNbr - contextLines + 1);
@@ -226,16 +232,14 @@
}
}
- private void appendFileLine(StringBuilder cmts, PatchFile fileData, short side, int line) {
- cmts.append("Line " + line);
- try {
- final String lineStr = fileData.getLine(side, line);
- cmts.append(": ");
- cmts.append(lineStr);
- } catch (Throwable e) {
- // Don't quote the line if we can't safely convert it.
- }
- cmts.append("\n");
+ private void appendFileLine(StringBuilder cmts, PatchFile fileData,
+ short side, int line) {
+ String lineStr = getLine(fileData, side, line);
+ cmts.append("Line ")
+ .append(line)
+ .append(": ")
+ .append(lineStr)
+ .append("\n");
}
private void appendQuotedParent(StringBuilder out, Comment child) {
@@ -293,4 +297,24 @@
soyContextEmailData.put("inlineComments", getInlineComments());
soyContextEmailData.put("hasInlineComments", hasInlineComments());
}
+
+ private String getLine(PatchFile fileInfo, short side, int lineNbr) {
+ try {
+ return fileInfo.getLine(side, lineNbr);
+ } catch (IOException err) {
+ // Default to the empty string if the file cannot be safely read.
+ log.warn(String.format("Failed to read file on side %d", side), err);
+ return "";
+ } catch (IndexOutOfBoundsException err) {
+ // Default to the empty string if the given line number does not appear
+ // in the file.
+ log.warn(String.format("Failed to get line number of file on side %d",
+ side), err);
+ return "";
+ } catch (NoSuchEntityException err) {
+ // Default to the empty string if the side cannot be found.
+ log.warn(String.format("Side %d of file didn't exist", side), err);
+ return "";
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java
index 9aef496..26bd99e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java
@@ -53,6 +53,7 @@
"FooterHtml.soy",
"HeaderHtml.soy",
"Merged.soy",
+ "MergedHtml.soy",
"NewChange.soy",
"NewChangeHtml.soy",
"RegisterNewEmail.soy",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index c2a3cdd..17a0854 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -59,6 +59,10 @@
@Override
protected void formatChange() throws EmailException {
appendText(textTemplate("Merged"));
+
+ if (useHtml()) {
+ appendHtml(soyHtmlTemplate("MergedHtml"));
+ }
}
public String getApprovals() {
@@ -129,4 +133,9 @@
super.setupSoyContext();
soyContextEmailData.put("approvals", getApprovals());
}
+
+ @Override
+ protected boolean supportsHtml() {
+ return true;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 70a5f4f..fa23b80 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -20,6 +20,7 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
@@ -45,6 +46,7 @@
protected final ChangeNoteUtil noteUtil;
protected final String anonymousCowardName;
protected final Account.Id accountId;
+ protected final Account.Id realAccountId;
protected final PersonIdent authorIdent;
protected final Date when;
@@ -69,6 +71,9 @@
this.notes = ctl.getNotes();
this.change = notes.getChange();
this.accountId = accountId(ctl.getUser());
+ Account.Id realAccountId = accountId(ctl.getUser().getRealUser());
+ this.realAccountId =
+ realAccountId != null ? realAccountId : accountId;
this.authorIdent =
ident(noteUtil, serverIdent, anonymousCowardName, ctl.getUser(), when);
this.when = when;
@@ -82,6 +87,7 @@
@Nullable ChangeNotes notes,
@Nullable Change change,
Account.Id accountId,
+ Account.Id realAccountId,
PersonIdent authorIdent,
Date when) {
checkArgument(
@@ -95,6 +101,7 @@
this.notes = notes;
this.change = change != null ? change : notes.getChange();
this.accountId = accountId;
+ this.realAccountId = realAccountId;
this.authorIdent = authorIdent;
this.when = when;
}
@@ -255,4 +262,18 @@
private static ObjectId emptyTree(ObjectInserter ins) throws IOException {
return ins.insert(Constants.OBJ_TREE, new byte[] {});
}
+
+ protected void verifyComment(Comment c) {
+ checkArgument(c.revId != null, "RevId required for comment: %s", c);
+ checkArgument(
+ c.author.getId().equals(getAccountId()),
+ "The author for the following comment does not match the author of"
+ + " this %s (%s): %s",
+ getClass().getSimpleName(), getAccountId(), c);
+ checkArgument(
+ c.getRealAuthor().getId().equals(realAccountId),
+ "The real author for the following comment does not match the real"
+ + " author of this %s (%s): %s",
+ getClass().getSimpleName(), realAccountId, c);
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
index ca595d7..3322776 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
@@ -226,13 +226,13 @@
checkColumns(Change.class,
1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18, 19, 101);
checkColumns(ChangeMessage.Key.class, 1, 2);
- checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5, 6);
+ checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5, 6, 7);
checkColumns(PatchSet.Id.class, 1, 2);
checkColumns(PatchSet.class, 1, 2, 3, 4, 5, 6, 8);
checkColumns(PatchSetApproval.Key.class, 1, 2, 3);
- checkColumns(PatchSetApproval.class, 1, 2, 3, 6);
+ checkColumns(PatchSetApproval.class, 1, 2, 3, 6, 7);
checkColumns(PatchLineComment.Key.class, 1, 2);
- checkColumns(PatchLineComment.class, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
+ checkColumns(PatchLineComment.class, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);
}
private final Change change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index b4ab290..57d5dce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.notedb;
import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.base.Preconditions.checkArgument;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.auto.value.AutoValue;
@@ -62,10 +61,19 @@
*/
public class ChangeDraftUpdate extends AbstractChangeUpdate {
public interface Factory {
- ChangeDraftUpdate create(ChangeNotes notes, Account.Id accountId,
- PersonIdent authorIdent, Date when);
- ChangeDraftUpdate create(Change change, Account.Id accountId,
- PersonIdent authorIdent, Date when);
+ ChangeDraftUpdate create(
+ ChangeNotes notes,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
+ PersonIdent authorIdent,
+ Date when);
+
+ ChangeDraftUpdate create(
+ Change change,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
+ PersonIdent authorIdent,
+ Date when);
}
@AutoValue
@@ -91,11 +99,12 @@
AllUsersName allUsers,
ChangeNoteUtil noteUtil,
@Assisted ChangeNotes notes,
- @Assisted Account.Id accountId,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
@Assisted PersonIdent authorIdent,
@Assisted Date when) {
super(migration, noteUtil, serverIdent, anonymousCowardName, notes, null,
- accountId, authorIdent, when);
+ accountId, realAccountId, authorIdent, when);
this.draftsProject = allUsers;
}
@@ -107,11 +116,12 @@
AllUsersName allUsers,
ChangeNoteUtil noteUtil,
@Assisted Change change,
- @Assisted Account.Id accountId,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
@Assisted PersonIdent authorIdent,
@Assisted Date when) {
super(migration, noteUtil, serverIdent, anonymousCowardName, null, change,
- accountId, authorIdent, when);
+ accountId, realAccountId, authorIdent, when);
this.draftsProject = allUsers;
}
@@ -129,12 +139,6 @@
delete.add(new AutoValue_ChangeDraftUpdate_Key(revId, key));
}
- private void verifyComment(Comment comment) {
- checkArgument(comment.author.getId().equals(accountId),
- "The author for the following comment does not match the author of"
- + " this ChangeDraftUpdate (%s): %s", accountId, comment);
- }
-
private CommitBuilder storeCommentsInNotes(RevWalk rw, ObjectInserter ins,
ObjectId curr, CommitBuilder cb)
throws ConfigInvalidException, OrmException, IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 078adf1..239b54e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -71,6 +71,7 @@
public static final FooterKey FOOTER_HASHTAGS = new FooterKey("Hashtags");
public static final FooterKey FOOTER_LABEL = new FooterKey("Label");
public static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");
+ public static final FooterKey FOOTER_REAL_USER = new FooterKey("Real-user");
public static final FooterKey FOOTER_STATUS = new FooterKey("Status");
public static final FooterKey FOOTER_SUBJECT = new FooterKey("Subject");
public static final FooterKey FOOTER_SUBMISSION_ID =
@@ -88,6 +89,7 @@
private static final String PARENT = "Parent";
private static final String PARENT_NUMBER = "Parent-number";
private static final String PATCH_SET = "Patch-set";
+ private static final String REAL_AUTHOR = "Real-author";
private static final String REVISION = "Revision";
private static final String UUID = "UUID";
private static final String TAG = FOOTER_TAG.getName();
@@ -232,7 +234,14 @@
}
Timestamp commentTime = parseTimestamp(note, curr, changeId);
- Account.Id aId = parseAuthor(note, curr, changeId);
+ Account.Id aId = parseAuthor(note, curr, changeId, AUTHOR);
+ boolean hasRealAuthor =
+ (RawParseUtils.match(note, curr.value, REAL_AUTHOR.getBytes(UTF_8)))
+ != -1;
+ Account.Id raId = null;
+ if (hasRealAuthor) {
+ raId = parseAuthor(note, curr, changeId, REAL_AUTHOR);
+ }
boolean hasParent =
(RawParseUtils.match(note, curr.value, PARENT.getBytes(UTF_8))) != -1;
@@ -269,6 +278,9 @@
c.parentUuid = parentUUID;
c.tag = tag;
c.setRevId(revId);
+ if (raId != null) {
+ c.setRealAuthor(raId);
+ }
if (range.getStartCharacter() != -1) {
c.setRange(range);
@@ -411,15 +423,15 @@
}
private Account.Id parseAuthor(byte[] note, MutableInteger curr,
- Change.Id changeId) throws ConfigInvalidException {
- checkHeaderLineFormat(note, curr, AUTHOR, changeId);
+ Change.Id changeId, String fieldName) throws ConfigInvalidException {
+ checkHeaderLineFormat(note, curr, fieldName, changeId);
int startOfAccountId =
RawParseUtils.endOfFooterLineKey(note, curr.value) + 2;
PersonIdent ident =
RawParseUtils.parsePersonIdent(note, startOfAccountId);
Account.Id aId = parseIdent(ident, changeId);
curr.value = RawParseUtils.nextLF(note, curr.value);
- return checkResult(aId, "comment author", changeId);
+ return checkResult(aId, fieldName, changeId);
}
private static int parseCommentLength(byte[] note, MutableInteger curr,
@@ -564,15 +576,10 @@
writer.print(formatTime(serverIdent, c.writtenOn));
writer.print("\n");
- PersonIdent ident = newIdent(
- accountCache.get(c.author.getId()).getAccount(),
- c.writtenOn, serverIdent, anonymousCowardName);
- StringBuilder name = new StringBuilder();
- PersonIdent.appendSanitized(name, ident.getName());
- name.append(" <");
- PersonIdent.appendSanitized(name, ident.getEmailAddress());
- name.append('>');
- appendHeaderField(writer, AUTHOR, name.toString());
+ appendIdent(writer, AUTHOR, c.author.getId(), c.writtenOn);
+ if (!c.getRealAuthor().equals(c.author)) {
+ appendIdent(writer, REAL_AUTHOR, c.getRealAuthor().getId(), c.writtenOn);
+ }
String parent = c.parentUuid;
if (parent != null) {
@@ -592,4 +599,17 @@
writer.print(c.message);
writer.print("\n\n");
}
+
+ private void appendIdent(PrintWriter writer, String header, Account.Id id,
+ Timestamp ts) {
+ PersonIdent ident = newIdent(
+ accountCache.get(id).getAccount(),
+ ts, serverIdent, anonymousCowardName);
+ StringBuilder name = new StringBuilder();
+ PersonIdent.appendSanitized(name, ident.getName());
+ name.append(" <");
+ PersonIdent.appendSanitized(name, ident.getEmailAddress());
+ name.append('>');
+ appendHeaderField(writer, header, name.toString());
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 360785f..edb5b4e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -30,6 +30,7 @@
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Ordering;
@@ -346,6 +347,11 @@
private DraftCommentNotes draftCommentNotes;
private RobotCommentNotes robotCommentNotes;
+ // Lazy defensive copies of mutable ReviewDb types, to avoid polluting the
+ // ChangeNotesCache from handlers.
+ private ImmutableMap<PatchSet.Id, PatchSet> patchSets;
+ private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals;
+
@VisibleForTesting
public ChangeNotes(Args args, Change change) {
this(args, change, true, null);
@@ -363,11 +369,19 @@
}
public ImmutableMap<PatchSet.Id, PatchSet> getPatchSets() {
- return state.patchSets();
+ if (patchSets == null) {
+ patchSets = ImmutableMap.copyOf(
+ Maps.transformValues(state.patchSets(), PatchSet::new));
+ }
+ return patchSets;
}
public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovals() {
- return state.approvals();
+ if (approvals == null) {
+ approvals = ImmutableListMultimap.copyOf(
+ Multimaps.transformValues(state.approvals(), PatchSetApproval::new));
+ }
+ return approvals;
}
public ReviewerSet getReviewers() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index 85df4b7..198eb2f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -33,6 +33,9 @@
import org.eclipse.jgit.lib.ObjectId;
import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@@ -49,7 +52,8 @@
cache(CACHE_NAME,
Key.class,
ChangeNotesState.class)
- .maximumWeight(1000);
+ .weigher(Weigher.class)
+ .maximumWeight(10 << 20);
}
};
}
@@ -61,6 +65,144 @@
abstract ObjectId id();
}
+ public static class Weigher
+ implements com.google.common.cache.Weigher<Key, ChangeNotesState> {
+ // Single object overhead.
+ private static final int O = 16;
+
+ // Single pointer overhead.
+ private static final int P = 8;
+
+ // Single IntKey overhead.
+ private static final int K = O + 4;
+
+ // Single Timestamp overhead.
+ private static final int T = O + 8;
+
+ @Override
+ public int weigh(Key key, ChangeNotesState state) {
+ // Take all columns and all collection sizes into account, but use
+ // estimated average element sizes rather than iterating over collections.
+ // Numbers are largely hand-wavy based on
+ // http://stackoverflow.com/questions/258120/what-is-the-memory-consumption-of-an-object-in-java
+ return
+ K // changeId
+ + str(40) // changeKey
+ + T // createdOn
+ + T // lastUpdatedOn
+ + P + K // owner
+ + P + str(state.columns().branch())
+ + P + patchSetId() // currentPatchSetId
+ + P + str(state.columns().subject())
+ + P + str(state.columns().topic())
+ + P + str(state.columns().originalSubject())
+ + P + str(state.columns().submissionId())
+ + ptr(state.columns().assignee(), K) // assignee
+ + P // status
+ + P + set(state.pastAssignees(), K)
+ + P + set(state.hashtags(), str(10))
+ + P + map(state.patchSets(), patchSet())
+ + P + list(state.reviewerUpdates(), 4 * O + K + K + P)
+ + P + list(state.submitRecords(), P + list(2, str(4) + P + K) + P)
+ + P + list(state.allChangeMessages(), changeMessage())
+ // Just key overhead for map, already counted messages in previous.
+ + P + map(state.changeMessagesByPatchSet().asMap(), patchSetId())
+ + P + map(state.publishedComments().asMap(), comment());
+ }
+
+ private static int ptr(Object o, int size) {
+ return o != null ? P + size : P;
+ }
+
+ private static int str(String s) {
+ if (s == null) {
+ return P;
+ }
+ return str(s.length());
+ }
+
+ private static int str(int n) {
+ return 8 + 24 + 2 * n;
+ }
+
+ private static int patchSetId() {
+ return O + 4 + O + 4;
+ }
+
+ private static int set(Set<?> set, int elemSize) {
+ if (set == null) {
+ return P;
+ }
+ return hashtable(set.size(), elemSize);
+ }
+
+ private static int map(Map<?, ?> map, int elemSize) {
+ if (map == null) {
+ return P;
+ }
+ return hashtable(map.size(), elemSize);
+ }
+
+ private static int hashtable(int n, int elemSize) {
+ // Made up numbers.
+ int overhead = 32;
+ int elemOverhead = O + 32;
+ return overhead + elemOverhead * n * elemSize;
+ }
+
+ private static int list(List<?> list, int elemSize) {
+ if (list == null) {
+ return P;
+ }
+ return list(list.size(), elemSize);
+ }
+
+ private static int list(int n, int elemSize) {
+ return O + O + n * (P + elemSize);
+ }
+
+ private static int patchSet() {
+ return O
+ + P + patchSetId()
+ + str(40) // revision
+ + P + K // uploader
+ + P + T // createdOn
+ + 1 // draft
+ + str(40) // groups
+ + P; // pushCertificate
+ }
+
+ private static int changeMessage() {
+ int key = K + str(20);
+ return O
+ + P + key
+ + P + K // author
+ + P + T // writtenON
+ + str(64) // message
+ + P + patchSetId()
+ + P
+ + P; // realAuthor
+ }
+
+ private static int comment() {
+ int key = P + str(20) + P + str(32) + 4;
+ int ident = O + 4;
+ return O
+ + P + key
+ + 4 // lineNbr
+ + P + ident // author
+ + P + ident //realAuthor
+ + P + T // writtenOn
+ + 2 // side
+ + str(32) // message
+ + str(10) // parentUuid
+ + (P + O + 4 + 4 + 4 + 4) / 2 // range on 50% of comments
+ + P // tag
+ + P + str(40) // revId
+ + P + str(36); // serverId
+ }
+ }
+
@AutoValue
abstract static class Value {
abstract ChangeNotesState state();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index de37b72..a3ef2ce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -22,6 +22,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
@@ -303,6 +304,7 @@
if (accountId != null) {
ownerId = accountId;
}
+ Account.Id realAccountId = parseRealAccountId(commit, accountId);
if (changeId == null) {
changeId = parseChangeId(commit);
@@ -316,7 +318,7 @@
originalSubject = currSubject;
}
- parseChangeMessage(psId, accountId, commit, ts);
+ parseChangeMessage(psId, accountId, realAccountId, commit, ts);
if (topic == null) {
topic = parseTopic(commit);
}
@@ -342,7 +344,7 @@
}
for (String line : commit.getFooterLineValues(FOOTER_LABEL)) {
- parseApproval(psId, accountId, ts, line);
+ parseApproval(psId, accountId, realAccountId, ts, line);
}
for (ReviewerStateInternal state : ReviewerStateInternal.values()) {
@@ -379,6 +381,16 @@
return parseOneFooter(commit, FOOTER_SUBJECT);
}
+ private Account.Id parseRealAccountId(ChangeNotesCommit commit,
+ Account.Id effectiveAccountId) throws ConfigInvalidException {
+ String realUser = parseOneFooter(commit, FOOTER_REAL_USER);
+ if (realUser == null) {
+ return effectiveAccountId;
+ }
+ PersonIdent ident = RawParseUtils.parsePersonIdent(realUser);
+ return noteUtil.parseIdent(ident, id);
+ }
+
private String parseTopic(ChangeNotesCommit commit)
throws ConfigInvalidException {
return parseOneFooter(commit, FOOTER_TOPIC);
@@ -565,7 +577,8 @@
}
private void parseChangeMessage(PatchSet.Id psId,
- Account.Id accountId, ChangeNotesCommit commit, Timestamp ts) {
+ Account.Id accountId, Account.Id realAccountId,
+ ChangeNotesCommit commit, Timestamp ts) {
byte[] raw = commit.getRawBuffer();
int size = raw.length;
Charset enc = RawParseUtils.parseEncoding(raw);
@@ -614,11 +627,10 @@
changeMessageStart, changeMessageEnd + 1);
ChangeMessage changeMessage = new ChangeMessage(
new ChangeMessage.Key(psId.getParentKey(), commit.name()),
- accountId,
- ts,
- psId);
+ accountId, ts, psId);
changeMessage.setMessage(changeMsgString);
changeMessage.setTag(tag);
+ changeMessage.setRealAuthor(realAccountId);
changeMessagesByPatchSet.put(psId, changeMessage);
allChangeMessages.add(changeMessage);
}
@@ -647,31 +659,45 @@
}
private void parseApproval(PatchSet.Id psId, Account.Id accountId,
- Timestamp ts, String line) throws ConfigInvalidException {
+ Account.Id realAccountId, Timestamp ts, String line)
+ throws ConfigInvalidException {
if (accountId == null) {
throw parseException(
"patch set %s requires an identified user as uploader", psId.get());
}
if (line.startsWith("-")) {
- parseRemoveApproval(psId, accountId, ts, line);
+ parseRemoveApproval(psId, accountId, realAccountId, ts, line);
} else {
- parseAddApproval(psId, accountId, ts, line);
+ parseAddApproval(psId, accountId, realAccountId, ts, line);
}
}
private void parseAddApproval(PatchSet.Id psId, Account.Id committerId,
- Timestamp ts, String line) throws ConfigInvalidException {
- Account.Id accountId;
+ Account.Id realAccountId, Timestamp ts, String line)
+ throws ConfigInvalidException {
+ // There are potentially 3 accounts involved here:
+ // 1. The account from the commit, which is the effective IdentifiedUser
+ // that produced the update.
+ // 2. The account in the label footer itself, which is used during submit
+ // to copy other users' labels to a new patch set.
+ // 3. The account in the Real-user footer, indicating that the whole
+ // update operation was executed by this user on behalf of the effective
+ // user.
+ Account.Id effectiveAccountId;
String labelVoteStr;
int s = line.indexOf(' ');
if (s > 0) {
+ // Account in the label line (2) becomes the effective ID of the
+ // approval. If there is a real user (3) different from the commit user
+ // (2), we actually don't store that anywhere in this case; it's more
+ // important to record that the real user (3) actually initiated submit.
labelVoteStr = line.substring(0, s);
PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
checkFooter(ident != null, FOOTER_LABEL, line);
- accountId = noteUtil.parseIdent(ident, id);
+ effectiveAccountId = noteUtil.parseIdent(ident, id);
} else {
labelVoteStr = line;
- accountId = committerId;
+ effectiveAccountId = committerId;
}
LabelVote l;
@@ -687,30 +713,36 @@
PatchSetApproval psa = new PatchSetApproval(
new PatchSetApproval.Key(
psId,
- accountId,
+ effectiveAccountId,
new LabelId(l.label())),
l.value(),
ts);
psa.setTag(tag);
- ApprovalKey k = ApprovalKey.create(psId, accountId, l.label(), tag);
+ if (!Objects.equals(realAccountId, committerId)) {
+ psa.setRealAccountId(realAccountId);
+ }
+ ApprovalKey k =
+ ApprovalKey.create(psId, effectiveAccountId, l.label(), tag);
if (!approvals.containsKey(k)) {
approvals.put(k, psa);
}
}
private void parseRemoveApproval(PatchSet.Id psId, Account.Id committerId,
- Timestamp ts, String line) throws ConfigInvalidException {
- Account.Id accountId;
+ Account.Id realAccountId, Timestamp ts, String line)
+ throws ConfigInvalidException {
+ // See comments in parseAddApproval about the various users involved.
+ Account.Id effectiveAccountId;
String label;
int s = line.indexOf(' ');
if (s > 0) {
label = line.substring(1, s);
PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
checkFooter(ident != null, FOOTER_LABEL, line);
- accountId = noteUtil.parseIdent(ident, id);
+ effectiveAccountId = noteUtil.parseIdent(ident, id);
} else {
label = line.substring(1);
- accountId = committerId;
+ effectiveAccountId = committerId;
}
try {
@@ -731,11 +763,14 @@
PatchSetApproval remove = new PatchSetApproval(
new PatchSetApproval.Key(
psId,
- accountId,
+ effectiveAccountId,
new LabelId(label)),
(short) 0,
ts);
- ApprovalKey k = ApprovalKey.create(psId, accountId, label, tag);
+ if (!Objects.equals(realAccountId, committerId)) {
+ remove.setRealAccountId(realAccountId);
+ }
+ ApprovalKey k = ApprovalKey.create(psId, effectiveAccountId, label, tag);
if (!approvals.containsKey(k)) {
approvals.put(k, remove);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 96cd4c6..3642dff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -27,6 +27,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMISSION_ID;
@@ -82,6 +83,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/**
@@ -100,8 +102,13 @@
public interface Factory {
ChangeUpdate create(ChangeControl ctl);
ChangeUpdate create(ChangeControl ctl, Date when);
- ChangeUpdate create(Change change, @Nullable Account.Id accountId,
- PersonIdent authorIdent, Date when,
+
+ ChangeUpdate create(
+ Change change,
+ @Assisted("effective") @Nullable Account.Id accountId,
+ @Assisted("real") @Nullable Account.Id realAccountId,
+ PersonIdent authorIdent,
+ Date when,
Comparator<String> labelNameComparator);
@VisibleForTesting
@@ -218,12 +225,13 @@
RobotCommentUpdate.Factory robotCommentUpdateFactory,
ChangeNoteUtil noteUtil,
@Assisted Change change,
- @Assisted @Nullable Account.Id accountId,
+ @Assisted("effective") @Nullable Account.Id accountId,
+ @Assisted("real") @Nullable Account.Id realAccountId,
@Assisted PersonIdent authorIdent,
@Assisted Date when,
@Assisted Comparator<String> labelNameComparator) {
super(migration, noteUtil, serverIdent, anonymousCowardName, null, change,
- accountId, authorIdent, when);
+ accountId, realAccountId, authorIdent, when);
this.accountCache = accountCache;
this.draftUpdateFactory = draftUpdateFactory;
this.robotCommentUpdateFactory = robotCommentUpdateFactory;
@@ -345,11 +353,11 @@
if (draftUpdate == null) {
ChangeNotes notes = getNotes();
if (notes != null) {
- draftUpdate =
- draftUpdateFactory.create(notes, accountId, authorIdent, when);
+ draftUpdate = draftUpdateFactory.create(
+ notes, accountId, realAccountId, authorIdent, when);
} else {
draftUpdate = draftUpdateFactory.create(
- getChange(), accountId, authorIdent, when);
+ getChange(), accountId, realAccountId, authorIdent, when);
}
}
return draftUpdate;
@@ -360,24 +368,16 @@
if (robotCommentUpdate == null) {
ChangeNotes notes = getNotes();
if (notes != null) {
- robotCommentUpdate =
- robotCommentUpdateFactory.create(notes, accountId, authorIdent, when);
+ robotCommentUpdate = robotCommentUpdateFactory.create(
+ notes, accountId, realAccountId, authorIdent, when);
} else {
robotCommentUpdate = robotCommentUpdateFactory.create(
- getChange(), accountId, authorIdent, when);
+ getChange(), accountId, realAccountId, authorIdent, when);
}
}
return robotCommentUpdate;
}
- private void verifyComment(Comment c) {
- checkArgument(c.revId != null, "RevId required for comment: %s", c);
- checkArgument(c.author.getId().equals(getAccountId()),
- "The author for the following comment does not match the author of"
- + " this ChangeUpdate (%s): %s", getAccountId(), c);
-
- }
-
public void setTopic(String topic) {
this.topic = Strings.nullToEmpty(topic);
}
@@ -646,10 +646,8 @@
addFooter(msg, FOOTER_SUBMITTED_WITH)
.append(label.status).append(": ").append(label.label);
if (label.appliedBy != null) {
- PersonIdent ident =
- newIdent(accountCache.get(label.appliedBy).getAccount(), when);
- msg.append(": ").append(ident.getName())
- .append(" <").append(ident.getEmailAddress()).append('>');
+ msg.append(": ");
+ addIdent(msg, label.appliedBy);
}
msg.append('\n');
}
@@ -657,6 +655,11 @@
}
}
+ if (!Objects.equals(accountId, realAccountId)) {
+ addFooter(msg, FOOTER_REAL_USER);
+ addIdent(msg, realAccountId).append('\n');
+ }
+
cb.setMessage(msg.toString());
try {
ObjectId treeId = storeRevisionNotes(rw, ins, curr);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
index fd47d02..9744632 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.notedb;
import static com.google.common.base.MoreObjects.firstNonNull;
-import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gerrit.reviewdb.client.RefNames.robotCommentsRef;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -55,12 +54,21 @@
* <p>
* This class is not thread safe.
*/
-public class RobotCommentUpdate extends AbstractChangeUpdate{
+public class RobotCommentUpdate extends AbstractChangeUpdate {
public interface Factory {
- RobotCommentUpdate create(ChangeNotes notes, Account.Id accountId,
- PersonIdent authorIdent, Date when);
- RobotCommentUpdate create(Change change, Account.Id accountId,
- PersonIdent authorIdent, Date when);
+ RobotCommentUpdate create(
+ ChangeNotes notes,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
+ PersonIdent authorIdent,
+ Date when);
+
+ RobotCommentUpdate create(
+ Change change,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
+ PersonIdent authorIdent,
+ Date when);
}
private List<RobotComment> put = new ArrayList<>();
@@ -72,11 +80,12 @@
NotesMigration migration,
ChangeNoteUtil noteUtil,
@Assisted ChangeNotes notes,
- @Assisted Account.Id accountId,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
@Assisted PersonIdent authorIdent,
@Assisted Date when) {
super(migration, noteUtil, serverIdent, anonymousCowardName, notes, null,
- accountId, authorIdent, when);
+ accountId, realAccountId, authorIdent, when);
}
@AssistedInject
@@ -86,11 +95,12 @@
NotesMigration migration,
ChangeNoteUtil noteUtil,
@Assisted Change change,
- @Assisted Account.Id accountId,
+ @Assisted("effective") Account.Id accountId,
+ @Assisted("real") Account.Id realAccountId,
@Assisted PersonIdent authorIdent,
@Assisted Date when) {
super(migration, noteUtil, serverIdent, anonymousCowardName, null, change,
- accountId, authorIdent, when);
+ accountId, realAccountId, authorIdent, when);
}
public void putComment(RobotComment c) {
@@ -98,12 +108,6 @@
put.add(c);
}
- private void verifyComment(RobotComment comment) {
- checkArgument(comment.author.getId().equals(accountId),
- "The author for the following comment does not match the author of"
- + " this RobotCommentUpdate (%s): %s", accountId, comment);
- }
-
private CommitBuilder storeCommentsInNotes(RevWalk rw, ObjectInserter ins,
ObjectId curr, CommitBuilder cb)
throws ConfigInvalidException, OrmException, IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
index a00334d..3aba7c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ApprovalEvent.java
@@ -23,8 +23,8 @@
private PatchSetApproval psa;
ApprovalEvent(PatchSetApproval psa, Timestamp changeCreatedOn) {
- super(psa.getPatchSetId(), psa.getAccountId(), psa.getGranted(),
- changeCreatedOn, psa.getTag());
+ super(psa.getPatchSetId(), psa.getAccountId(), psa.getRealAccountId(),
+ psa.getGranted(), changeCreatedOn, psa.getTag());
this.psa = psa;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
index a990e19..ed5cd8b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
@@ -31,17 +31,12 @@
private static final Pattern TOPIC_REMOVED_REGEXP =
Pattern.compile("^Topic (.+) removed$");
- private static final Pattern STATUS_ABANDONED_REGEXP =
- Pattern.compile("^Abandoned(\n.*)*$");
- private static final Pattern STATUS_RESTORED_REGEXP =
- Pattern.compile("^Restored(\n.*)*$");
-
private final ChangeMessage message;
private final Change noteDbChange;
ChangeMessageEvent(ChangeMessage message, Change noteDbChange,
Timestamp changeCreatedOn) {
- super(message.getPatchSetId(), message.getAuthor(),
+ super(message.getPatchSetId(), message.getAuthor(), message.getRealAuthor(),
message.getWrittenOn(), changeCreatedOn, message.getTag());
this.message = message;
this.noteDbChange = noteDbChange;
@@ -57,7 +52,6 @@
checkUpdate(update);
update.setChangeMessage(message.getMessage());
setTopic(update);
- setStatus(update);
}
private void setTopic(ChangeUpdate update) {
@@ -86,21 +80,4 @@
noteDbChange.setTopic(null);
}
}
-
- private void setStatus(ChangeUpdate update) {
- String msg = message.getMessage();
- if (msg == null) {
- return;
- }
- if (STATUS_ABANDONED_REGEXP.matcher(msg).matches()) {
- update.setStatus(Change.Status.ABANDONED);
- noteDbChange.setStatus(Change.Status.ABANDONED);
- return;
- }
-
- if (STATUS_RESTORED_REGEXP.matcher(msg).matches()) {
- update.setStatus(Change.Status.NEW);
- noteDbChange.setStatus(Change.Status.NEW);
- }
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
index d4caa6c..1916610 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -355,19 +355,16 @@
Change noteDbChange = new Change(null, null, null, null, null);
for (ChangeMessage msg : bundle.getChangeMessages()) {
- if (msg.getPatchSetId() == null) {
- // No dependency necessary; will get assigned to most recent patch set
- // in sortAndFillEvents.
- events.add(
- new ChangeMessageEvent(msg, noteDbChange, change.getCreatedOn()));
- continue;
+ List<Event> msgEvents = parseChangeMessage(msg, change, noteDbChange);
+ if (msg.getPatchSetId() != null) {
+ PatchSetEvent pse = patchSetEvents.get(msg.getPatchSetId());
+ if (pse != null) {
+ for (Event e : msgEvents) {
+ e.addDep(pse);
+ }
+ }
}
- PatchSetEvent pse = patchSetEvents.get(msg.getPatchSetId());
- if (pse != null) {
- events.add(
- new ChangeMessageEvent(msg, noteDbChange, change.getCreatedOn())
- .addDep(pse));
- }
+ events.addAll(msgEvents);
}
sortAndFillEvents(change, noteDbChange, events, minPsNum);
@@ -396,6 +393,18 @@
}
}
+ private List<Event> parseChangeMessage(ChangeMessage msg, Change change,
+ Change noteDbChange) {
+ List<Event> events = new ArrayList<>(2);
+ events.add(new ChangeMessageEvent(msg, noteDbChange, change.getCreatedOn()));
+ Optional<StatusChangeEvent> sce =
+ StatusChangeEvent.parseFromMessage(msg, change, noteDbChange);
+ if (sce.isPresent()) {
+ events.add(sce.get());
+ }
+ return events;
+ }
+
private static Integer getMinPatchSetNum(ChangeBundle bundle) {
Integer minPsNum = null;
for (PatchSet ps : bundle.getPatchSets()) {
@@ -418,13 +427,14 @@
private void sortAndFillEvents(Change change, Change noteDbChange,
List<Event> events, Integer minPsNum) {
- new EventSorter(events).sort();
events.add(new FinalUpdatesEvent(change, noteDbChange));
+ new EventSorter(events).sort();
// Ensure the first event in the list creates the change, setting the author
// and any required footers.
Event first = events.get(0);
- if (first instanceof PatchSetEvent && change.getOwner().equals(first.who)) {
+ if (first instanceof PatchSetEvent
+ && change.getOwner().equals(first.user)) {
((PatchSetEvent) first).createChange = true;
} else {
events.add(0, new CreateChangeEvent(change, minPsNum));
@@ -483,6 +493,7 @@
ChangeUpdate update = updateFactory.create(
change,
events.getAccountId(),
+ events.getRealAccountId(),
newAuthorIdent(events),
events.getWhen(),
labelNameComparator);
@@ -505,6 +516,7 @@
ChangeDraftUpdate update = draftUpdateFactory.create(
change,
events.getAccountId(),
+ events.getRealAccountId(),
newAuthorIdent(events),
events.getWhen());
update.setPatchSetId(events.getPatchSetId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
index 51ade79..8f461a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CommentEvent.java
@@ -34,7 +34,7 @@
CommentEvent(Comment c, Change change, PatchSet ps,
PatchListCache cache) {
super(CommentsUtil.getCommentPsId(change.getId(), c), c.author.getId(),
- c.writtenOn, change.getCreatedOn(), c.tag);
+ c.getRealAuthor().getId(), c.writtenOn, change.getCreatedOn(), c.tag);
this.c = c;
this.change = change;
this.ps = ps;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CreateChangeEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CreateChangeEvent.java
index 886f6c4..b020911 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CreateChangeEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/CreateChangeEvent.java
@@ -37,8 +37,8 @@
}
CreateChangeEvent(Change change, Integer minPsNum) {
- super(psId(change, minPsNum), change.getOwner(), change.getCreatedOn(),
- change.getCreatedOn(), null);
+ super(psId(change, minPsNum), change.getOwner(), change.getOwner(),
+ change.getCreatedOn(), change.getCreatedOn(), null);
this.change = change;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
index 360dfff..2938480 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/DraftCommentEvent.java
@@ -34,7 +34,7 @@
DraftCommentEvent(Comment c, Change change, PatchSet ps,
PatchListCache cache) {
super(CommentsUtil.getCommentPsId(change.getId(), c), c.author.getId(),
- c.writtenOn, change.getCreatedOn(), c.tag);
+ c.getRealAuthor().getId(), c.writtenOn, change.getCreatedOn(), c.tag);
this.c = c;
this.change = change;
this.ps = ps;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java
index d04d1b5..78df2d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/Event.java
@@ -36,17 +36,24 @@
// NOTE: EventList only supports direct subclasses, not an arbitrary
// hierarchy.
- final Account.Id who;
+ final Account.Id user;
+ final Account.Id realUser;
final String tag;
final boolean predatesChange;
final List<Event> deps;
Timestamp when;
PatchSet.Id psId;
- protected Event(PatchSet.Id psId, Account.Id who, Timestamp when,
- Timestamp changeCreatedOn, String tag) {
+ protected Event(
+ PatchSet.Id psId,
+ Account.Id effectiveUser,
+ Account.Id realUser,
+ Timestamp when,
+ Timestamp changeCreatedOn,
+ String tag) {
this.psId = psId;
- this.who = who;
+ this.user = effectiveUser;
+ this.realUser = realUser != null ? realUser : effectiveUser;
this.tag = tag;
// Truncate timestamps at the change's createdOn timestamp.
predatesChange = when.before(changeCreatedOn);
@@ -61,9 +68,9 @@
checkState(when.getTime() - update.getWhen().getTime() <= MAX_WINDOW_MS,
"event at %s outside update window starting at %s",
when, update.getWhen());
- checkState(Objects.equals(update.getNullableAccountId(), who),
+ checkState(Objects.equals(update.getNullableAccountId(), user),
"cannot apply event by %s to update by %s",
- who, update.getNullableAccountId());
+ user, update.getNullableAccountId());
}
Event addDep(Event e) {
@@ -79,15 +86,12 @@
abstract void apply(ChangeUpdate update) throws OrmException, IOException;
- protected boolean isPatchSet() {
- return false;
- }
-
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("psId", psId)
- .add("who", who)
+ .add("effectiveUser", user)
+ .add("realUser", realUser)
.add("when", when)
.toString();
}
@@ -95,12 +99,23 @@
@Override
public int compareTo(Event other) {
return ComparisonChain.start()
+ .compareFalseFirst(this.isFinalUpdates(), other.isFinalUpdates())
.compare(this.when, other.when)
.compareTrueFirst(isPatchSet(), isPatchSet())
.compareTrueFirst(this.predatesChange, other.predatesChange)
- .compare(this.who, other.who, ReviewDbUtil.intKeyOrdering())
+ .compare(this.user, other.user,
+ ReviewDbUtil.intKeyOrdering())
+ .compare(this.realUser, other.realUser, ReviewDbUtil.intKeyOrdering())
.compare(this.psId, other.psId,
ReviewDbUtil.intKeyOrdering().nullsLast())
.result();
}
+
+ private boolean isPatchSet() {
+ return this instanceof PatchSetEvent;
+ }
+
+ private boolean isFinalUpdates() {
+ return this instanceof FinalUpdatesEvent;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java
index 398657b..4f6e2e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/EventList.java
@@ -49,7 +49,8 @@
}
Event last = getLast();
- if (!Objects.equals(e.who, last.who)
+ if (!Objects.equals(e.user, last.user)
+ || !Objects.equals(e.realUser, last.realUser)
|| !e.psId.equals(last.psId)
|| !Objects.equals(e.tag, last.tag)) {
return false; // Different patch set, author, or tag.
@@ -93,10 +94,19 @@
}
Account.Id getAccountId() {
- Account.Id id = get(0).who;
+ Account.Id id = get(0).user;
for (int i = 1; i < size(); i++) {
- checkState(Objects.equals(id, get(i).who),
- "mismatched users in EventList: %s != %s", id, get(i).who);
+ checkState(Objects.equals(id, get(i).user),
+ "mismatched users in EventList: %s != %s", id, get(i).user);
+ }
+ return id;
+ }
+
+ Account.Id getRealAccountId() {
+ Account.Id id = get(0).realUser;
+ for (int i = 1; i < size(); i++) {
+ checkState(Objects.equals(id, get(i).realUser),
+ "mismatched real users in EventList: %s != %s", id, get(i).realUser);
}
return id;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
index 3080be7..9babc28 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
@@ -25,7 +25,7 @@
private final Change noteDbChange;
FinalUpdatesEvent(Change change, Change noteDbChange) {
- super(change.currentPatchSetId(), change.getOwner(),
+ super(change.currentPatchSetId(), change.getOwner(), change.getOwner(),
change.getLastUpdatedOn(), change.getCreatedOn(), null);
this.change = change;
this.noteDbChange = noteDbChange;
@@ -46,9 +46,14 @@
// TODO(dborowitz): Stamp approximate approvals at this time.
update.fixStatus(change.getStatus());
}
- if (change.getSubmissionId() != null) {
+ if (change.getSubmissionId() != null
+ && noteDbChange.getSubmissionId() == null) {
update.setSubmissionId(change.getSubmissionId());
}
+ if (!Objects.equals(change.getAssignee(), noteDbChange.getAssignee())) {
+ // TODO(dborowitz): Parse intermediate values out from messages.
+ update.setAssignee(change.getAssignee());
+ }
if (!update.isEmpty()) {
update.setSubjectForCommit("Final NoteDb migration updates");
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java
index 21b3b6e..f5bea3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/HashtagsEvent.java
@@ -27,7 +27,7 @@
HashtagsEvent(PatchSet.Id psId, Account.Id who, Timestamp when,
Set<String> hashtags, Timestamp changeCreatdOn) {
- super(psId, who, when, changeCreatdOn,
+ super(psId, who, who, when, changeCreatdOn,
// Somewhat confusingly, hashtags do not use the setTag method on
// AbstractChangeUpdate, so pass null as the tag.
null);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/PatchSetEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/PatchSetEvent.java
index 5baddd3..abac1b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/PatchSetEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/PatchSetEvent.java
@@ -35,7 +35,7 @@
boolean createChange;
PatchSetEvent(Change change, PatchSet ps, RevWalk rw) {
- super(ps.getId(), ps.getUploader(), ps.getCreatedOn(),
+ super(ps.getId(), ps.getUploader(), ps.getUploader(), ps.getCreatedOn(),
change.getCreatedOn(), null);
this.change = change;
this.ps = ps;
@@ -66,11 +66,6 @@
}
}
- @Override
- protected boolean isPatchSet() {
- return true;
- }
-
private void setRevision(ChangeUpdate update, PatchSet ps)
throws IOException {
String rev = ps.getRevision().get();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java
index ef9c5c6..c82f108 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ReviewerEvent.java
@@ -34,7 +34,13 @@
// (although as an implementation detail they were in ReviewDb). Just
// use the latest patch set at the time of the event.
null,
- reviewer.getColumnKey(), reviewer.getValue(), changeCreatedOn, null);
+ reviewer.getColumnKey(),
+ // TODO(dborowitz): Real account ID shouldn't really matter for
+ // reviewers, but we might have to deal with this to avoid ChangeBundle
+ // diffs when run against real data.
+ reviewer.getColumnKey(),
+ reviewer.getValue(),
+ changeCreatedOn, null);
this.reviewer = reviewer;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java
new file mode 100644
index 0000000..c7632e8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/StatusChangeEvent.java
@@ -0,0 +1,90 @@
+// 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.notedb.rebuild;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableMap;
+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;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gwtorm.server.OrmException;
+
+import java.sql.Timestamp;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+class StatusChangeEvent extends Event {
+ private static final ImmutableMap<Change.Status, Pattern> PATTERNS =
+ ImmutableMap.of(
+ Change.Status.ABANDONED, Pattern.compile("^Abandoned(\n.*)*$"),
+ Change.Status.MERGED, Pattern.compile(
+ "^Change has been successfully"
+ + " (merged|cherry-picked|rebased|pushed).*$"),
+ Change.Status.NEW, Pattern.compile("^Restored(\n.*)*$"));
+
+ static Optional<StatusChangeEvent> parseFromMessage(ChangeMessage message,
+ Change change, Change noteDbChange) {
+ String msg = message.getMessage();
+ if (msg == null) {
+ return Optional.absent();
+ }
+ for (Map.Entry<Change.Status, Pattern> e : PATTERNS.entrySet()) {
+ if (e.getValue().matcher(msg).matches()) {
+ return Optional.of(new StatusChangeEvent(
+ message, change, noteDbChange, e.getKey()));
+ }
+ }
+ return Optional.absent();
+ }
+
+ private final Change change;
+ private final Change noteDbChange;
+ private final Change.Status status;
+
+ private StatusChangeEvent(ChangeMessage message, Change change,
+ Change noteDbChange, Change.Status status) {
+ this(message.getPatchSetId(), message.getAuthor(),
+ message.getWrittenOn(), change, noteDbChange, message.getTag(),
+ status);
+ }
+
+ private StatusChangeEvent(PatchSet.Id psId, Account.Id author,
+ Timestamp when, Change change, Change noteDbChange,
+ String tag, Change.Status status) {
+ super(psId, author, author, when, change.getCreatedOn(), tag);
+ this.change = change;
+ this.noteDbChange = noteDbChange;
+ this.status = status;
+ }
+
+ @Override
+ boolean uniquePerUpdate() {
+ return true;
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ void apply(ChangeUpdate update) throws OrmException {
+ checkUpdate(update);
+ update.fixStatus(status);
+ noteDbChange.setStatus(status);
+ if (status == Change.Status.MERGED) {
+ update.setSubmissionId(change.getSubmissionId());
+ noteDbChange.setSubmissionId(change.getSubmissionId());
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index fa385f2..939d8d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -42,8 +42,8 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
@@ -103,7 +103,7 @@
private final GitReferenceUpdated referenceUpdated;
private final RepositoryConfig repositoryCfg;
private final PersonIdent serverIdent;
- private final Provider<CurrentUser> currentUser;
+ private final Provider<IdentifiedUser> identifiedUser;
private final Provider<PutConfig> putConfig;
private final AllProjectsName allProjects;
private final String name;
@@ -122,7 +122,7 @@
GitReferenceUpdated referenceUpdated,
RepositoryConfig repositoryCfg,
@GerritPersonIdent PersonIdent serverIdent,
- Provider<CurrentUser> currentUser,
+ Provider<IdentifiedUser> identifiedUser,
Provider<PutConfig> putConfig,
AllProjectsName allProjects,
@Assisted String name) {
@@ -140,7 +140,7 @@
this.referenceUpdated = referenceUpdated;
this.repositoryCfg = repositoryCfg;
this.serverIdent = serverIdent;
- this.currentUser = currentUser;
+ this.identifiedUser = identifiedUser;
this.putConfig = putConfig;
this.allProjects = allProjects;
this.name = name;
@@ -214,8 +214,8 @@
if (input.pluginConfigValues != null) {
try {
- ProjectControl projectControl =
- projectControlFactory.controlFor(p.getNameKey(), currentUser.get());
+ ProjectControl projectControl = projectControlFactory.controlFor(
+ p.getNameKey(), identifiedUser.get());
ConfigInput in = new ConfigInput();
in.pluginConfigValues = input.pluginConfigValues;
putConfig.get().apply(projectControl, in);
@@ -355,7 +355,7 @@
switch (result) {
case NEW:
referenceUpdated.fire(project, ru, ReceiveCommand.Type.CREATE,
- currentUser.get().getAccountId());
+ identifiedUser.get().getAccount());
break;
case FAST_FORWARD:
case FORCED:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java
index be6f892..8effe44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java
@@ -245,7 +245,10 @@
info.max = r.getMax();
info.min = r.getMin();
}
- pInfo.rules.put(r.getGroup().getUUID().get(), info);
+ AccountGroup.UUID group = r.getGroup().getUUID();
+ if (group != null) {
+ pInfo.rules.put(group.get(), info);
+ }
}
accessSectionInfo.permissions.put(p.getName(), pInfo);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
index 4574a7a..1e5a7c9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
@@ -267,7 +267,9 @@
r.setMin(pri.min);
}
r.setAction(GetAccess.ACTION_TYPE.inverse().get(pri.action));
- r.setForce(pri.force);
+ if (pri.force != null) {
+ r.setForce(pri.force);
+ }
}
p.add(r);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
index 3a21ce4..644ed63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
@@ -353,6 +353,9 @@
} catch (RuntimeException | IllegalAccessException e) {
throw error("Error in operator " + name + ":" + value, e);
} catch (InvocationTargetException e) {
+ if (e.getCause() instanceof QueryParseException) {
+ throw (QueryParseException) e.getCause();
+ }
throw error("Error in operator " + name + ":" + value, e.getCause());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
index 0997a40..ae88887 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
@@ -162,7 +162,7 @@
int page = (start / limit) + 1;
if (page > indexConfig.maxPages()) {
throw new QueryParseException(
- "Cannot go beyond page " + indexConfig.maxPages() + "of results");
+ "Cannot go beyond page " + indexConfig.maxPages() + " of results");
}
// Always bump limit by 1, even if this results in exceeding the permitted
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 5d05daf..57f5710 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -418,7 +418,8 @@
}
@Operator
- public Predicate<ChangeData> status(String statusName) {
+ public Predicate<ChangeData> status(String statusName)
+ throws QueryParseException {
if ("reviewed".equalsIgnoreCase(statusName)) {
return IsReviewedPredicate.create();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 1c92ecf..1ae8591 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -18,6 +18,7 @@
import com.google.gerrit.reviewdb.client.Change.Status;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
import com.google.gwtorm.server.OrmException;
import java.util.ArrayList;
@@ -63,7 +64,8 @@
return status.name().toLowerCase();
}
- public static Predicate<ChangeData> parse(String value) {
+ public static Predicate<ChangeData> parse(String value)
+ throws QueryParseException {
String lower = value.toLowerCase();
NavigableMap<String, Predicate<ChangeData>> head =
PREDICATES.tailMap(lower, true);
@@ -75,7 +77,7 @@
return e.getValue();
}
}
- throw new IllegalArgumentException("invalid change status: " + value);
+ throw new QueryParseException("invalid change status: " + value);
}
public static Predicate<ChangeData> open() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index ab14d8b..a7a8191 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -33,7 +33,7 @@
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
- public static final Class<Schema_132> C = Schema_132.class;
+ public static final Class<Schema_133> C = Schema_133.class;
public static int getBinaryVersion() {
return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_133.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_133.java
new file mode 100644
index 0000000..31d330b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_133.java
@@ -0,0 +1,25 @@
+// 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.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_133 extends SchemaVersion {
+ @Inject
+ Schema_133(Provider<Schema_132> prior) {
+ super(prior);
+ }
+}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy
new file mode 100644
index 0000000..33dd7b8
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy
@@ -0,0 +1,53 @@
+/**
+ * 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.
+ */
+
+{namespace com.google.gerrit.server.mail.template}
+
+/**
+ * @param change
+ * @param email
+ * @param fromName
+ */
+{template .MergedHtml autoescape="strict" kind="html"}
+ <p>
+ {$fromName} has submitted this change and it was merged.
+ </p>
+
+ {if $email.changeUrl}
+ <p>
+ {call .ViewChangeButton data="all" /}
+ </p>
+ {/if}
+
+ <p>
+ Change subject: {$change.subject}
+ </p>
+ <hr/>
+
+ <pre>
+ {$email.changeDetail}
+ </pre>
+
+ <pre>
+ {$email.approvals}
+ </pre>
+
+ {if $email.includeDiff}
+ <pre>
+ {$email.unifiedDiff}
+ </pre>
+ {/if}
+{/template}
\ No newline at end of file
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
index 222fb14..bf36738 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/RepositoryConfigTest.java
@@ -145,7 +145,6 @@
RepositoryConfig.OWNER_GROUP_NAME, ownerGroups);
}
- @SuppressWarnings("cast")
@Test
public void testBasePathWhenNotConfigured() {
assertThat((Object)repoCfg.getBasePath(new NameKey("someProject"))).isNull();
@@ -159,7 +158,6 @@
.isEqualTo(basePath);
}
- @SuppressWarnings("cast")
@Test
public void testBasePathForSpecificFilter() {
String basePath = "/someAbsolutePath/someDirectory";
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 393b8f8..ea33e65 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -46,6 +46,7 @@
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.config.GerritServerId;
@@ -1666,6 +1667,59 @@
}
@Test
+ public void patchLineCommentNotesFormatRealAuthor() throws Exception {
+ Change c = newChange();
+ CurrentUser ownerAsOtherUser =
+ userFactory.runAs(null, otherUserId, changeOwner);
+ ChangeUpdate update = newUpdate(c, ownerAsOtherUser);
+ String uuid = "uuid";
+ String message = "comment";
+ CommentRange range = new CommentRange(1, 1, 2, 1);
+ Timestamp time = TimeUtil.nowTs();
+ PatchSet.Id psId = c.currentPatchSetId();
+ RevId revId = new RevId("abcd1234abcd1234abcd1234abcd1234abcd1234");
+
+ Comment comment = newComment(psId, "file", uuid, range,
+ range.getEndLine(), otherUser, null, time, message, (short) 1,
+ revId.get());
+ comment.setRealAuthor(changeOwner.getAccountId());
+ update.setPatchSetId(psId);
+ update.putComment(Status.PUBLISHED, comment);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+
+ try (RevWalk walk = new RevWalk(repo)) {
+ ArrayList<Note> notesInTree =
+ Lists.newArrayList(notes.revisionNoteMap.noteMap.iterator());
+ Note note = Iterables.getOnlyElement(notesInTree);
+
+ byte[] bytes =
+ walk.getObjectReader().open(
+ note.getData(), Constants.OBJ_BLOB).getBytes();
+ String noteString = new String(bytes, UTF_8);
+
+ if (!testJson()) {
+ assertThat(noteString).isEqualTo(
+ "Revision: abcd1234abcd1234abcd1234abcd1234abcd1234\n"
+ + "Patch-set: 1\n"
+ + "File: file\n"
+ + "\n"
+ + "1:1-2:1\n"
+ + ChangeNoteUtil.formatTime(serverIdent, time) + "\n"
+ + "Author: Other Account <2@gerrit>\n"
+ + "Real-author: Change Owner <1@gerrit>\n"
+ + "UUID: uuid\n"
+ + "Bytes: 7\n"
+ + "comment\n"
+ + "\n");
+ }
+ }
+ assertThat(notes.getComments())
+ .isEqualTo(ImmutableMultimap.of(revId, comment));
+ }
+
+ @Test
public void patchLineCommentNotesFormatWeirdUser() throws Exception {
Account account = new Account(new Account.Id(3), TimeUtil.nowTs());
account.setFullName("Weird\n\u0002<User>\n");
@@ -2355,6 +2409,21 @@
assertThat(comments.get(1).message).isEqualTo("comment 2");
}
+ @Test
+ public void realUser() throws Exception {
+ Change c = newChange();
+ CurrentUser ownerAsOtherUser =
+ userFactory.runAs(null, otherUserId, changeOwner);
+ ChangeUpdate update = newUpdate(c, ownerAsOtherUser);
+ update.setChangeMessage("Message on behalf of other user");
+ update.commit();
+
+ ChangeMessage msg = Iterables.getLast(newNotes(c).getChangeMessages());
+ assertThat(msg.getMessage()).isEqualTo("Message on behalf of other user");
+ assertThat(msg.getAuthor()).isEqualTo(otherUserId);
+ assertThat(msg.getRealAuthor()).isEqualTo(changeOwner.getAccountId());
+ }
+
private boolean testJson() {
return noteUtil.getWriteJson();
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 5a8c12b..b674159 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -22,6 +22,7 @@
import com.google.gerrit.common.TimeUtil;
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.util.RequestId;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.TestChanges;
@@ -332,6 +333,29 @@
update.getResult());
}
+ @Test
+ public void realUser() throws Exception {
+ Change c = newChange();
+ CurrentUser ownerAsOtherUser =
+ userFactory.runAs(null, otherUserId, changeOwner);
+ ChangeUpdate update = newUpdate(c, ownerAsOtherUser);
+ update.setChangeMessage("Message on behalf of other user");
+ update.commit();
+
+ RevCommit commit = parseCommit(update.getResult());
+ PersonIdent author = commit.getAuthorIdent();
+ assertThat(author.getName()).isEqualTo("Other Account");
+ assertThat(author.getEmailAddress()).isEqualTo("2@gerrit");
+
+ assertBodyEquals("Update patch set 1\n"
+ + "\n"
+ + "Message on behalf of other user\n"
+ + "\n"
+ + "Patch-set: 1\n"
+ + "Real-user: Change Owner <1@gerrit>\n",
+ commit);
+ }
+
private RevCommit parseCommit(ObjectId id) throws Exception {
if (id instanceof RevCommit) {
return (RevCommit) id;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/rebuild/EventSorterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/rebuild/EventSorterTest.java
index 969adf0..f5fdf13 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/rebuild/EventSorterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/rebuild/EventSorterTest.java
@@ -37,7 +37,7 @@
protected TestEvent(Timestamp when) {
super(
new PatchSet.Id(new Change.Id(1), 1),
- new Account.Id(1000),
+ new Account.Id(1000), new Account.Id(1000),
when, changeCreatedOn, null);
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 59ae73f..4758338 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -21,7 +21,6 @@
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
-import static org.junit.Assert.fail;
import com.google.common.base.MoreObjects;
import com.google.common.collect.FluentIterable;
@@ -30,6 +29,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.common.truth.ThrowableSubject;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.GerritApi;
@@ -244,7 +244,8 @@
assertQuery("change:repo~branch~" + k.substring(0, 10), change);
assertQuery("foo~bar");
- assertBadQuery("change:foo~bar");
+ assertThatQueryException("change:foo~bar")
+ .hasMessage("Invalid change format");
assertQuery("otherrepo~branch~" + k);
assertQuery("change:otherrepo~branch~" + k);
assertQuery("repo~otherbranch~" + k);
@@ -342,8 +343,10 @@
assertQuery("status:N", change1);
assertQuery("status:nE", change1);
assertQuery("status:neW", change1);
- assertBadQuery("status:nx");
- assertBadQuery("status:newx");
+ assertThatQueryException("status:nx")
+ .hasMessage("invalid change status: nx");
+ assertThatQueryException("status:newx")
+ .hasMessage("invalid change status: newx");
}
@Test
@@ -764,7 +767,8 @@
assertQuery(query, change);
assertQuery(query.withStart(1));
assertQuery(query.withStart(99));
- assertBadQuery(query.withStart(100));
+ assertThatQueryException(query.withStart(100))
+ .hasMessage("Cannot go beyond page 10 of results");
assertQuery(query.withLimit(100).withStart(100));
}
@@ -1613,16 +1617,19 @@
return inserter.getChange();
}
- protected void assertBadQuery(Object query) throws Exception {
- assertBadQuery(newQuery(query));
+ protected ThrowableSubject assertThatQueryException(Object query)
+ throws Exception {
+ return assertThatQueryException(newQuery(query));
}
- protected void assertBadQuery(QueryRequest query) throws Exception {
+ protected ThrowableSubject assertThatQueryException(QueryRequest query)
+ throws Exception {
try {
query.get();
- fail("expected BadRequestException for query: " + query);
+ throw new AssertionError(
+ "expected BadRequestException for query: " + query);
} catch (BadRequestException e) {
- // Expected.
+ return assertThat(e);
}
}
diff --git a/lib/BUCK b/lib/BUCK
index 14ae9df..5ec54f7 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -246,8 +246,8 @@
maven_jar(
name = 'truth',
- id = 'com.google.truth:truth:0.28',
- sha1 = '0a388c7877c845ff4b8e19689dda5ac9d34622c4',
+ id = 'com.google.truth:truth:0.30',
+ sha1 = '9d591b5a66eda81f0b88cf1c748ab8853d99b18b',
license = 'DO_NOT_DISTRIBUTE',
exported_deps = [
':guava',
diff --git a/lib/asciidoctor/BUILD b/lib/asciidoctor/BUILD
index 4b4e958..d1b98f8 100644
--- a/lib/asciidoctor/BUILD
+++ b/lib/asciidoctor/BUILD
@@ -1,3 +1,10 @@
+java_binary(
+ name = "asciidoc",
+ main_class = "AsciiDoctor",
+ runtime_deps = [":asciidoc_lib"],
+ visibility = ["//visibility:public"],
+)
+
java_library(
name = "asciidoc_lib",
srcs = ["java/AsciiDoctor.java"],
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java
index 8e18feb1..c66c4aa 100644
--- a/lib/asciidoctor/java/AsciiDoctor.java
+++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -24,12 +24,14 @@
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
-
+import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
+import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
+import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -41,6 +43,7 @@
private static final String DOCTYPE = "article";
private static final String ERUBY = "erb";
+ private static final String REVNUMBER_NAME = "revnumber";
@Option(name = "-b", usage = "set output format backend")
private String backend = "html5";
@@ -60,13 +63,26 @@
@Option(name = "--tmp", usage = "temporary output path")
private File tmpdir;
+ @Option(name = "--mktmp", usage = "create a temporary output path")
+ private boolean mktmp;
+
@Option(name = "-a", usage =
"a list of attributes, in the form key or key=value pair")
private List<String> attributes = new ArrayList<>();
+ @Option(name = "--bazel", usage =
+ "bazel mode: generate multiple output files instead of a single zip file")
+ private boolean bazel;
+
+ @Option(name = "--revnumber-file", usage =
+ "the file contains revnumber string")
+ private File revnumberFile;
+
@Argument(usage = "input files")
private List<String> inputFiles = new ArrayList<>();
+ private String revnumber;
+
public static String mapInFileToOutFile(
String inFile, String inExt, String outExt) {
String basename = new File(inFile).getName();
@@ -82,19 +98,26 @@
return basename + outExt;
}
- private Options createOptions(File outputFile) {
+ private Options createOptions(File base, File outputFile) {
OptionsBuilder optionsBuilder = OptionsBuilder.options();
- optionsBuilder.backend(backend).docType(DOCTYPE).eruby(ERUBY)
- .safe(SafeMode.UNSAFE).baseDir(basedir);
- // XXX(fishywang): ideally we should just output to a string and add the
- // content into zip. But asciidoctor will actually ignore all attributes if
- // not output to a file. So we *have* to output to a file then read the
- // content of the file into zip.
- optionsBuilder.toFile(outputFile);
+ optionsBuilder
+ .backend(backend)
+ .docType(DOCTYPE)
+ .eruby(ERUBY)
+ .safe(SafeMode.UNSAFE)
+ .baseDir(base)
+ // XXX(fishywang): ideally we should just output to a string and add the
+ // content into zip. But asciidoctor will actually ignore all attributes
+ // if not output to a file. So we *have* to output to a file then read
+ // the content of the file into zip.
+ .toFile(outputFile);
AttributesBuilder attributesBuilder = AttributesBuilder.attributes();
attributesBuilder.attributes(getAttributes());
+ if (revnumber != null) {
+ attributesBuilder.attribute(REVNUMBER_NAME, revnumber);
+ }
optionsBuilder.attributes(attributesBuilder.get());
return optionsBuilder.get();
@@ -133,31 +156,52 @@
return;
}
- try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile))) {
- for (String inputFile : inputFiles) {
- if (!inputFile.endsWith(inExt)) {
- // We have to use UNSAFE mode in order to make embedding work. But in
- // UNSAFE mode we'll also need css file in the same directory, so we
- // have to add css files into the SRCS.
- continue;
- }
-
- String outName = mapInFileToOutFile(inputFile, inExt, outExt);
- File out = new File(tmpdir, outName);
- out.getParentFile().mkdirs();
- Options options = createOptions(out);
- renderInput(options, new File(inputFile));
- zipFile(out, outName, zip);
+ if (revnumberFile != null) {
+ try (BufferedReader reader =
+ new BufferedReader(new FileReader(revnumberFile))) {
+ revnumber = reader.readLine();
}
+ }
- File[] cssFiles = tmpdir.listFiles(new FilenameFilter() {
- @Override
- public boolean accept(File dir, String name) {
- return name.endsWith(".css");
+ if (mktmp) {
+ tmpdir = Files.createTempDirectory("asciidoctor-").toFile();
+ }
+
+ if (bazel) {
+ renderFiles(inputFiles, null);
+ } else {
+ try (ZipOutputStream zip =
+ new ZipOutputStream(new FileOutputStream(zipFile))) {
+ renderFiles(inputFiles, zip);
+
+ File[] cssFiles = tmpdir.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.endsWith(".css");
+ }
+ });
+ for (File css : cssFiles) {
+ zipFile(css, css.getName(), zip);
}
- });
- for (File css : cssFiles) {
- zipFile(css, css.getName(), zip);
+ }
+ }
+ }
+
+ private void renderFiles(List<String> inputFiles, ZipOutputStream zip)
+ throws IOException {
+ Asciidoctor asciidoctor = JRubyAsciidoctor.create();
+ for (String inputFile : inputFiles) {
+ String outName = mapInFileToOutFile(inputFile, inExt, outExt);
+ File out = bazel ? new File(outName) : new File(tmpdir, outName);
+ if (!bazel) {
+ out.getParentFile().mkdirs();
+ }
+ File input = new File(inputFile);
+ Options options =
+ createOptions(basedir != null ? basedir : input.getParentFile(), out);
+ asciidoctor.renderFile(input, options);
+ if (zip != null) {
+ zipFile(out, outName, zip);
}
}
}
@@ -171,11 +215,6 @@
zip.closeEntry();
}
- private void renderInput(Options options, File inputFile) {
- Asciidoctor asciidoctor = JRubyAsciidoctor.create();
- asciidoctor.renderFile(inputFile, options);
- }
-
public static void main(String[] args) {
try {
new AsciiDoctor().invoke(args);
diff --git a/lib/js/BUILD b/lib/js/BUILD
index 3030013..249f5d0 100644
--- a/lib/js/BUILD
+++ b/lib/js/BUILD
@@ -27,3 +27,18 @@
srcs = [ "//lib/highlightjs:highlight.min.js" ],
license = '//lib:LICENSE-highlightjs',
)
+
+bower_component(
+ name = 'iron-test-helpers',
+ seed = True,
+)
+
+bower_component(
+ name = 'test-fixture',
+ seed = True,
+)
+
+bower_component(
+ name = 'web-component-tester',
+ seed = True,
+)
diff --git a/lib/prolog/prolog.bzl b/lib/prolog/prolog.bzl
index d4e9e08..995d81c 100644
--- a/lib/prolog/prolog.bzl
+++ b/lib/prolog/prolog.bzl
@@ -18,7 +18,7 @@
name,
srcs,
deps = [],
- visibility = []):
+ **kwargs):
genrule2(
name = name + '__pl2j',
cmd = '$(location //lib/prolog:compiler_bin) ' +
@@ -32,5 +32,5 @@
name = name,
srcs = [':' + name + '__pl2j'],
deps = ['//lib/prolog:runtime'] + deps,
- visibility = visibility,
+ **kwargs
)
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index 6378bfc..9191ab7 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -1,9 +1,12 @@
+package(
+ default_visibility=["//visibility:public"]
+)
load("//tools/bzl:js.bzl", "bower_component_bundle")
load('//tools/bzl:genrule2.bzl', 'genrule2')
bower_component_bundle(
- name = "components",
+ name = "polygerrit_components",
deps = [
'//lib/js:es6-promise',
'//lib/js:fetch',
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 383fb50..1e548d5 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -13,9 +13,21 @@
All other platforms: [download from
nodejs.org](https://nodejs.org/en/download/).
-## Optional: installing [go](https://golang.org/)
+## Installing [Buck](https://buckbuild.com/)
-This is only required for running the ```run-server.sh``` script for testing. See below.
+Follow the instructions
+[here](https://gerrit-review.googlesource.com/Documentation/dev-buck.html#_installation)
+to get and install Buck.
+
+## Local UI, Production Data
+
+This is a quick and easy way to test your local changes against real data.
+Unfortunately, you can't sign in, so testing certain features will require
+you to use the "test data" technique described below.
+
+### Installing [go](https://golang.org/)
+
+This is required for running the `run-server.sh` script below.
```sh
# Debian/Ubuntu
@@ -27,18 +39,18 @@
All other platforms: [download from golang.org](https://golang.org/)
-# Add [go] to your path
+Then add go to your path:
```
PATH=$PATH:/usr/local/go/bin
```
-## Local UI, Production Data
+### Running the server
To test the local UI against gerrit-review.googlesource.com:
```sh
-./polygerrit-ui/run-server.sh
+./run-server.sh
```
Then visit http://localhost:8081
@@ -47,10 +59,8 @@
One-time setup:
-1. [Install Buck](https://gerrit-review.googlesource.com/Documentation/dev-buck.html#_installation)
- for building Gerrit.
-2. [Build Gerrit](https://gerrit-review.googlesource.com/Documentation/dev-buck.html#_gerrit_development_war_file)
- and set up a local test site. Docs
+1. [Build Gerrit](https://gerrit-review.googlesource.com/Documentation/dev-buck.html#_gerrit_development_war_file)
+2. Set up a local test site. Docs
[here](https://gerrit-review.googlesource.com/Documentation/install-quick.html) and
[here](https://gerrit-review.googlesource.com/Documentation/dev-readme.html#init).
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
new file mode 100644
index 0000000..29bbfff
--- /dev/null
+++ b/polygerrit-ui/app/BUILD
@@ -0,0 +1,37 @@
+package(
+ default_visibility = ["//visibility:public"])
+load("//tools/bzl:js.bzl", "bower_component_bundle", "vulcanize")
+
+WCT_TEST_PATTERNS = [
+ 'test/*.js',
+ 'test/*.html',
+ '**/*_test.html',
+]
+PY_TEST_PATTERNS = ['polygerrit_wct_tests.py']
+APP_SRCS = glob(
+ ['**'],
+ exclude = [
+ 'BUCK',
+ '*~',
+ '**/BUILD',
+ 'index.html',
+ 'test/**',
+ ] + WCT_TEST_PATTERNS + PY_TEST_PATTERNS)
+
+
+bower_component_bundle(
+ name = 'test_components',
+ deps = [
+ '//polygerrit-ui:polygerrit_components',
+ '//lib/js:iron-test-helpers',
+ '//lib/js:test-fixture',
+ '//lib/js:web-component-tester',
+ ],
+)
+
+vulcanize(
+ name = "gr-app",
+ app = 'elements/gr-app.html',
+ srcs = APP_SRCS,
+ deps = [ "//polygerrit-ui:polygerrit_components"],
+)
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
similarity index 94%
rename from polygerrit-ui/app/behaviors/gr-path-list-behavior.html
rename to polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
index fefecd4..fa8289f 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
@@ -31,11 +31,11 @@
var aLastDotIndex = a.lastIndexOf('.');
var aExt = a.substr(aLastDotIndex + 1);
- var aFile = a.substr(0, aLastDotIndex);
+ var aFile = a.substr(0, aLastDotIndex) || a;
var bLastDotIndex = b.lastIndexOf('.');
var bExt = b.substr(bLastDotIndex + 1);
- var bFile = b.substr(0, bLastDotIndex);
+ var bFile = b.substr(0, bLastDotIndex) || b;
// Sort header files above others with the same base name.
var headerExts = ['h', 'hxx', 'hpp'];
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
new file mode 100644
index 0000000..530b7be
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -0,0 +1,37 @@
+<!--
+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.
+-->
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<title>gr-path-list-behavior</title>
+
+<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="gr-path-list-behavior.html">
+
+<script>
+ suite('gr-path-list-behavior tests', function() {
+ test('special sort', function() {
+ var sort = Gerrit.PathListBehavior.specialFilePathCompare;
+ var testFiles = [
+ '/a.h',
+ '/a.cpp',
+ '/COMMIT_MSG',
+ '/asdasd',
+ '/mrPeanutbutter.py'
+ ];
+ assert.deepEqual(testFiles.sort(sort),
+ ['/COMMIT_MSG', '/a.h', '/a.cpp', '/asdasd', '/mrPeanutbutter.py']);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 09d5b84..96c6193 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -66,6 +66,7 @@
<section hidden$="[[!_actionCount(actions.*, _additionalActions.*)]]">
<template is="dom-repeat" items="[[_changeActionValues]]" as="action">
<gr-button title$="[[action.title]]"
+ hidden$="[[_computeActionHidden(action.__key, _hiddenChangeActions.*)]]"
primary$="[[action.__primary]]"
hidden$="[[!action.enabled]]"
data-action-key$="[[action.__key]]"
@@ -78,6 +79,7 @@
<section hidden$="[[!_actionCount(_revisionActions.*, _additionalActions.*)]]">
<template is="dom-repeat" items="[[_revisionActionValues]]" as="action">
<gr-button title$="[[action.title]]"
+ hidden$="[[_computeActionHidden(action.__key, _hiddenRevisionActions.*)]]"
primary$="[[action.__primary]]"
disabled$="[[!action.enabled]]"
data-action-key$="[[action.__key]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 89c06c4..e35d3cb 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -102,6 +102,14 @@
type: Array,
value: function() { return []; },
},
+ _hiddenChangeActions: {
+ type: Array,
+ value: function() { return []; },
+ },
+ _hiddenRevisionActions: {
+ type: Array,
+ value: function() { return []; },
+ },
},
ActionType: ActionType,
@@ -169,6 +177,24 @@
], value);
},
+ setActionHidden: function(type, key, hidden) {
+ var path;
+ if (type === ActionType.CHANGE) {
+ path = '_hiddenChangeActions';
+ } else if (type === ActionType.REVISION) {
+ path = '_hiddenRevisionActions';
+ } else {
+ throw Error('Invalid action type given: ' + type);
+ }
+
+ var idx = this.get(path).indexOf(key);
+ if (hidden && idx === -1) {
+ this.push(path, key);
+ } else if (!hidden && idx !== -1) {
+ this.splice(path, idx, 1);
+ }
+ },
+
_indexOfActionButtonWithKey: function(key) {
for (var i = 0; i < this._additionalActions.length; i++) {
if (this._additionalActions[i].__key === key) {
@@ -270,6 +296,12 @@
this._getRevision(this.change, this.patchNum));
},
+ _computeActionHidden: function(key, hiddenActionsChangeRecord) {
+ var hiddenActions =
+ (hiddenActionsChangeRecord && hiddenActionsChangeRecord.base) || [];
+ return hiddenActions.indexOf(key) !== -1;
+ },
+
_getRevision: function(change, patchNum) {
var num = window.parseInt(patchNum, 10);
for (var hash in change.revisions) {
@@ -283,7 +315,14 @@
_modifyRevertMsg: function() {
return this.$.jsAPI.modifyRevertMsg(this.change,
- this.$.confirmRevertDialog.message);
+ this.$.confirmRevertDialog.message, this.commitMessage);
+ },
+
+ showRevertDialog: function() {
+ this.$.confirmRevertDialog.populateRevertMessage(
+ this.commitMessage, this.change.current_revision);
+ this.$.confirmRevertDialog.message = this._modifyRevertMsg();
+ this._showActionDialog(this.$.confirmRevertDialog);
},
_handleActionTap: function(e) {
@@ -298,9 +337,7 @@
if (type === ActionType.REVISION) {
this._handleRevisionAction(key);
} else if (key === ChangeActions.REVERT) {
- this.$.confirmRevertDialog.populateRevertMessage(this.commitMessage);
- this.$.confirmRevertDialog.message = this._modifyRevertMsg();
- this._showActionDialog(this.$.confirmRevertDialog);
+ this.showRevertDialog();
} else if (key === ChangeActions.ABANDON) {
this._showActionDialog(this.$.confirmAbandonDialog);
} else {
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 bbd51e2..5fe41ec 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
@@ -97,6 +97,64 @@
return element.reload();
});
+ test('hide revision action', function(done) {
+ flush(function() {
+ var buttonEl = element.$$('[data-action-key="submit"]');
+ assert.isOk(buttonEl);
+ assert.isFalse(buttonEl.hasAttribute('hidden'));
+ assert.throws(element.setActionHidden.bind(element, 'invalid type'));
+ element.setActionHidden(element.ActionType.REVISION,
+ element.RevisionActions.SUBMIT, true);
+ assert.lengthOf(element._hiddenRevisionActions, 1);
+ element.setActionHidden(element.ActionType.REVISION,
+ element.RevisionActions.SUBMIT, true);
+ assert.lengthOf(element._hiddenRevisionActions, 1);
+ flush(function() {
+ var buttonEl = element.$$('[data-action-key="submit"]');
+ assert.isOk(buttonEl);
+ assert.isTrue(buttonEl.hasAttribute('hidden'));
+
+ element.setActionHidden(element.ActionType.REVISION,
+ element.RevisionActions.SUBMIT, false);
+ flush(function() {
+ var buttonEl = element.$$('[data-action-key="submit"]');
+ assert.isOk(buttonEl);
+ assert.isFalse(buttonEl.hasAttribute('hidden'));
+ done();
+ });
+ });
+ });
+ });
+
+ test('hide change action', function(done) {
+ flush(function() {
+ var buttonEl = element.$$('[data-action-key="/"]');
+ assert.isOk(buttonEl);
+ assert.isFalse(buttonEl.hasAttribute('hidden'));
+ assert.throws(element.setActionHidden.bind(element, 'invalid type'));
+ element.setActionHidden(element.ActionType.CHANGE,
+ element.ChangeActions.DELETE, true);
+ assert.lengthOf(element._hiddenChangeActions, 1);
+ element.setActionHidden(element.ActionType.CHANGE,
+ element.ChangeActions.DELETE, true);
+ assert.lengthOf(element._hiddenChangeActions, 1);
+ flush(function() {
+ var buttonEl = element.$$('[data-action-key="/"]');
+ assert.isOk(buttonEl);
+ assert.isTrue(buttonEl.hasAttribute('hidden'));
+
+ element.setActionHidden(element.ActionType.CHANGE,
+ element.RevisionActions.DELETE, false);
+ flush(function() {
+ var buttonEl = element.$$('[data-action-key="/"]');
+ assert.isOk(buttonEl);
+ assert.isFalse(buttonEl.hasAttribute('hidden'));
+ done();
+ });
+ });
+ });
+ });
+
test('buttons show', function(done) {
flush(function() {
var buttonEls = Polymer.dom(element.root).querySelectorAll('gr-button');
@@ -334,6 +392,9 @@
});
test('revert change with plugin hook', function(done) {
+ element.change = {
+ current_revision: 'abc1234',
+ };
var newRevertMsg = 'Modified revert msg';
var modifyRevertMsgStub = sinon.stub(element, '_modifyRevertMsg',
function() { return newRevertMsg; });
@@ -353,6 +414,9 @@
});
test('works', function() {
+ element.change = {
+ current_revision: 'abc1234',
+ };
var populateRevertMsgStub = sinon.stub(
element.$.confirmRevertDialog, 'populateRevertMessage',
function() { return 'original msg'; });
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 28935ce..661a296 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -88,7 +88,7 @@
_handleTopicChanged: function(e, topic) {
if (!topic.length) { topic = null; }
- this.$.restAPI.setChangeTopic(this.change.id, topic);
+ this.$.restAPI.setChangeTopic(this.change.change_id, topic);
},
_computeTopicReadOnly: function(mutable, change) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index 218e124..22080e8 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -85,6 +85,8 @@
sandbox.stub(element, '_computeValueTooltip').returns('');
sandbox.stub(element, '_computeTopicReadOnly').returns(true);
element.change = {
+ change_id: 'the id',
+ topic: 'the topic',
status: 'NEW',
submit_type: 'CHERRY_PICK',
labels: {
@@ -145,6 +147,13 @@
done();
});
});
+
+ test('changing topic calls setChangeTopic', function() {
+ var topicStub = sandbox.stub(element.$.restAPI, 'setChangeTopic',
+ function() {});
+ element._handleTopicChanged({}, 'the new topic');
+ assert.isTrue(topicStub.calledWith('the id', 'the new topic'));
+ });
});
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index a4b0a9b..3d4e10d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -224,6 +224,10 @@
mutable="[[_loggedIn]]"
on-show-reply-dialog="_handleShowReplyDialog">
</gr-change-metadata>
+ <!-- Plugins insert content into following container.
+ Stop-gap until PolyGerrit plugins interface is ready.
+ This will not work with Shadow DOM. -->
+ <div id="change_plugins"></div>
</div>
<div class="changeInfo-column mainChangeInfo">
<div class="commitActions" hidden$="[[!_loggedIn]]"">
@@ -298,7 +302,8 @@
drafts="[[_diffDrafts]]"
revisions="[[_change.revisions]]"
projectConfig="[[_projectConfig]]"
- selected-index="{{viewState.selectedFileIndex}}"></gr-file-list>
+ selected-index="{{viewState.selectedFileIndex}}"
+ diff-view-mode="{{viewState.diffMode}}"></gr-file-list>
</section>
<gr-messages-list id="messageList"
change-num="[[_changeNum]]"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 9f90800..3d1cac7e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -42,6 +42,7 @@
notify: true,
value: function() { return {}; },
},
+ backPage: String,
serverConfig: Object,
keyEventTarget: {
type: Object,
@@ -368,6 +369,8 @@
this._maybeShowReplyDialog();
+ this._maybeShowRevertDialog();
+
this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, {
change: this._change,
patchNum: this._patchRange.patchNum,
@@ -395,6 +398,35 @@
}
},
+ _getLocationSearch: function() {
+ // Not inlining to make it easier to test.
+ return window.location.search;
+ },
+
+ _getUrlParameter: function(param) {
+ var pageURL = this._getLocationSearch().substring(1);
+ var vars = pageURL.split('&');
+ for (var i = 0; i < vars.length; i++) {
+ var name = vars[i].split('=');
+ if (name[0] == param) {
+ return name[0];
+ }
+ }
+ return null;
+ },
+
+ _maybeShowRevertDialog: function() {
+ this._getLoggedIn().then(function(loggedIn) {
+ if (!loggedIn || this._change.status !== this.ChangeStatus.MERGED) {
+ // Do not display dialog if not logged-in or the change is not merged.
+ return;
+ }
+ if (!!this._getUrlParameter('revert')) {
+ this.$.actions.showRevertDialog();
+ }
+ }.bind(this));
+ },
+
_maybeShowReplyDialog: function() {
this._getLoggedIn().then(function(loggedIn) {
if (!loggedIn) { return; }
@@ -573,11 +605,17 @@
break;
case 85: // 'u'
e.preventDefault();
- page.show('/');
+ this._determinePageBack();
break;
}
},
+ _determinePageBack: function() {
+ // Default backPage to '/' if user came to change view page
+ // via an email link, etc.
+ page.show(this.backPage || '/');
+ },
+
_labelsChanged: function(changeRecord) {
if (!changeRecord) { return; }
this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index b819837..8cc4343 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -45,13 +45,21 @@
});
suite('keyboard shortcuts', function() {
- test('U should navigate to /', function() {
+ test('U should navigate to / if no backPage set', function() {
var showStub = sinon.stub(page, 'show');
MockInteractions.pressAndReleaseKeyOn(element, 85); // 'U'
assert(showStub.lastCall.calledWithExactly('/'));
showStub.restore();
});
+ test('U should navigate to backPage if set', function() {
+ element.backPage = '/dashboard/self';
+ var showStub = sinon.stub(page, 'show');
+ MockInteractions.pressAndReleaseKeyOn(element, 85); // 'U'
+ assert(showStub.lastCall.calledWithExactly('/dashboard/self'));
+ showStub.restore();
+ });
+
test('A should toggle overlay', function() {
MockInteractions.pressAndReleaseKeyOn(element, 65); // 'A'
var overlayEl = element.$.replyOverlay;
@@ -445,6 +453,60 @@
assert.equal(element._computePatchInfoClass('4', allPatcheSets), '');
});
+ test('getUrlParameter functionality', function() {
+ var locationStub = sinon.stub(element, '_getLocationSearch');
+
+ locationStub.returns('?test');
+ assert.equal(element._getUrlParameter('test'), 'test');
+ locationStub.returns('?test2=12&test=3');
+ assert.equal(element._getUrlParameter('test'), 'test');
+ locationStub.returns('');
+ assert.isNull(element._getUrlParameter('test'));
+ locationStub.returns('?');
+ assert.isNull(element._getUrlParameter('test'));
+ locationStub.returns('?test2');
+ assert.isNull(element._getUrlParameter('test'));
+
+ locationStub.restore();
+ });
+
+ test('revert dialog opened with revert param', function(done) {
+ sinon.stub(element.$.restAPI, 'getLoggedIn', function() {
+ return Promise.resolve(true);
+ });
+
+ element._patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 2,
+ };
+ element._change = {
+ change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+ revisions: {
+ rev1: {_number: 1},
+ },
+ current_revision: 'rev1',
+ status: element.ChangeStatus.MERGED,
+ labels: {},
+ actions: {},
+ };
+
+ var urlParamStub = sinon.stub(element, '_getUrlParameter',
+ function(param) {
+ assert.equal(param, 'revert');
+ urlParamStub.restore();
+ element.$.restAPI.getLoggedIn.restore();
+ return param;
+ });
+
+ var revertDialogStub = sinon.stub(element.$.actions, 'showRevertDialog',
+ function() {
+ revertDialogStub.restore();
+ done();
+ });
+
+ element._maybeShowRevertDialog();
+ });
+
suite('scroll related tests', function() {
test('document scrolling calls function to set scroll height',
function(done) {
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index d462c12..e4ab44b 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -14,7 +14,7 @@
limitations under the License.
-->
-<link rel="import" href="../../../behaviors/gr-path-list-behavior.html">
+<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html">
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
index 979a06a..b668d06 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
@@ -55,6 +55,7 @@
</label>
<iron-autogrow-textarea
id="messageInput"
+ max-rows="15"
class="message"
bind-value="{{message}}"></iron-autogrow-textarea>
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index 0dbefc5..8f621f0 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -33,25 +33,19 @@
message: String,
},
- populateRevertMessage: function(message) {
+ populateRevertMessage: function(message, commitHash) {
// Figure out what the revert title should be.
var originalTitle = message.split('\n')[0];
var revertTitle = 'Revert "' + originalTitle + '"';
- // Figure out what the revert commit message should be.
- var commitRegex = /\nChange-Id: (\w+)\n\s*/g;
- var match = commitRegex.exec(message);
- if (!match) {
- alert('Unable to find Change-Id in footer of commit message.');
+ if (!commitHash) {
+ alert('Unable to find the commit hash of this change.');
return;
}
- var revertCommitText = 'This reverts commit ' + match[1] + '.';
- // Add '> ' in front of the original commit text.
- var originalCommitText = message.replace(/^/gm, '> ');
+ var revertCommitText = 'This reverts commit ' + commitHash + '.';
this.message = revertTitle + '\n\n' +
revertCommitText + '\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n\n' +
- 'Original change\'s description:\n' + originalCommitText;
+ 'Reason for revert: <INSERT REASONING HERE>\n';
},
_handleConfirmTap: function(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
index 8ccfc9a..f5672d3 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
@@ -41,60 +41,52 @@
test('no match', function() {
assert.isNotOk(element.message);
var alertStub = sinon.stub(window, 'alert');
- element.populateRevertMessage('not a change-id in sight');
+ element.populateRevertMessage('not a commitHash in sight', undefined);
assert.isTrue(alertStub.calledOnce);
alertStub.restore();
});
test('single line', function() {
assert.isNotOk(element.message);
- element.populateRevertMessage('one line commit\n\nChange-Id: abcdefg\n');
+ element.populateRevertMessage(
+ 'one line commit\n\nChange-Id: abcdefg\n',
+ 'abcd123');
var expected = 'Revert "one line commit"\n\n' +
- 'This reverts commit abcdefg.\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n\n' +
- 'Original change\'s description:\n' +
- '> one line commit\n> \n' +
- '> Change-Id: abcdefg\n> ';
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
assert.equal(element.message, expected);
});
test('multi line', function() {
assert.isNotOk(element.message);
element.populateRevertMessage(
- 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n');
+ 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n',
+ 'abcd123');
var expected = 'Revert "many lines"\n\n' +
- 'This reverts commit abcdefg.\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n\n' +
- 'Original change\'s description:\n' +
- '> many lines\n> commit\n> \n> message\n> \n' +
- '> Change-Id: abcdefg\n> ';
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
assert.equal(element.message, expected);
});
test('issue above change id', function() {
assert.isNotOk(element.message);
element.populateRevertMessage(
- 'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n');
+ 'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n',
+ 'abcd123');
var expected = 'Revert "much lines"\n\n' +
- 'This reverts commit abcdefg.\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n\n' +
- 'Original change\'s description:\n' +
- '> much lines\n> very\n> \n> commit\n> \n' +
- '> Bug: Issue 42\n' +
- '> Change-Id: abcdefg\n> ';
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
assert.equal(element.message, expected);
});
test('revert a revert', function() {
assert.isNotOk(element.message);
element.populateRevertMessage(
- 'Revert "one line commit"\n\nChange-Id: abcdefg\n');
+ 'Revert "one line commit"\n\nChange-Id: abcdefg\n',
+ 'abcd123');
var expected = 'Revert "Revert "one line commit""\n\n' +
- 'This reverts commit abcdefg.\n\n' +
- 'Reason for revert: <INSERT REASONING HERE>\n\n' +
- 'Original change\'s description:\n' +
- '> Revert "one line commit"\n> \n' +
- '> Change-Id: abcdefg\n> ';
+ 'This reverts commit abcd123.\n\n' +
+ 'Reason for revert: <INSERT REASONING HERE>\n';
assert.equal(element.message, expected);
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index dcb2ca7..78e4634 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -89,7 +89,8 @@
.invisible {
visibility: hidden;
}
- .row:not(.header) .stats {
+ .row:not(.header) .stats,
+ .total-stats {
font-family: var(--monospace-font-family);
}
.added {
@@ -108,6 +109,11 @@
.fileListButton {
margin: .5em;
}
+ .totalChanges {
+ justify-content: flex-end;
+ padding-right: 2.6em;
+ text-align: right;
+ }
input.show-hide {
display: none;
}
@@ -147,9 +153,18 @@
/
<gr-button link on-tap="_collapseAllDiffs">Hide diffs</gr-button>
/
+ <select
+ id="modeSelect"
+ is="gr-select"
+ bind-value="{{diffViewMode}}"
+ on-change="_handleDropdownChange">
+ <option value="SIDE_BY_SIDE">Side By Side</option>
+ <option value="UNIFIED_DIFF">Unified</option>
+ </select>
+ /
<label>
Diff against
- <select on-change="_handlePatchChange">
+ <select id="patchChange" on-change="_handlePatchChange">
<option value="PARENT">Base</option>
<template is="dom-repeat" items="[[_computePatchSets(revisions, patchRange.*)]]" as="patchNum">
<option
@@ -201,7 +216,8 @@
</div>
</div>
<gr-diff
- hidden$="[[_computeHiddenState(file.__expanded)]]"
+ hidden$="[[!file.__expanded]]"
+ expanded="[[file.__expanded]]"
project="[[change.project]]"
commit="[[change.current_revision]]"
change-num="[[changeNum]]"
@@ -209,8 +225,14 @@
path="[[file.__path]]"
prefs="[[_diffPrefs]]"
project-config="[[projectConfig]]"
- view-mode="[[_userPrefs.diff_view]]"></gr-diff>
+ view-mode="[[_diffMode]]"></gr-diff>
</template>
+ <div class="row totalChanges">
+ <div class="total-stats" hidden$="[[_hideChangeTotals]]">
+ <span class="added">+[[_patchChange.inserted]]</span>
+ <span class="removed">-[[_patchChange.deleted]]</span>
+ </div>
+ </div>
<gr-button
class="fileListButton"
id="incrementButton"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index dae798d..478faf5 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -16,6 +16,11 @@
var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
+ var DiffViewMode = {
+ SIDE_BY_SIDE: 'SIDE_BY_SIDE',
+ UNIFIED: 'UNIFIED_DIFF',
+ };
+
Polymer({
is: 'gr-file-list',
@@ -36,7 +41,10 @@
value: function() { return document.body; },
},
change: Object,
-
+ diffViewMode: {
+ type: String,
+ notify: true,
+ },
_files: {
type: Array,
observer: '_filesChanged',
@@ -58,15 +66,27 @@
type: Number,
value: 75,
},
+ _patchChange: {
+ type: Object,
+ computed: '_calculatePatchChange(_files)',
+ },
_fileListIncrement: {
type: Number,
readOnly: true,
value: 75,
},
+ _hideChangeTotals: {
+ type: Boolean,
+ computed: '_shouldHideChangeTotals(_patchChange)',
+ },
_shownFiles: {
type: Array,
computed: '_computeFilesShown(_numFilesShown, _files.*)',
},
+ _diffMode: {
+ type: String,
+ computed: '_getDiffViewMode(diffViewMode, _userPrefs)',
+ },
},
behaviors: [
@@ -100,8 +120,17 @@
this._diffPrefs = prefs;
}.bind(this)));
+ // Initialize with user's diff mode preference. Default to
+ // SIDE_BY_SIDE in the meantime.
+ var setDiffViewMode = this.diffViewMode === null;
+ if (setDiffViewMode) {
+ this.set('diffViewMode', DiffViewMode.SIDE_BY_SIDE);
+ }
promises.push(this._getPreferences().then(function(prefs) {
this._userPrefs = prefs;
+ if (setDiffViewMode) {
+ this.set('diffViewMode', prefs.diff_view);
+ }
}.bind(this)));
},
@@ -109,6 +138,22 @@
return Polymer.dom(this.root).querySelectorAll('gr-diff');
},
+ _calculatePatchChange: function(files) {
+ var filesNoCommitMsg = files.filter(function(files) {
+ return files.__path !== '/COMMIT_MSG';
+ });
+
+ return filesNoCommitMsg.reduce(function(acc, obj) {
+ var inserted = obj.lines_inserted ? obj.lines_inserted : 0;
+ var deleted = obj.lines_deleted ? obj.lines_deleted : 0;
+
+ return {
+ inserted: acc.inserted + inserted,
+ deleted: acc.deleted + deleted,
+ };
+ }, {inserted: 0, deleted: 0});
+ },
+
_getDiffPreferences: function() {
return this.$.restAPI.getDiffPreferences();
},
@@ -402,6 +447,10 @@
window.scrollTo(0, top - document.body.clientHeight / 2);
},
+ _shouldHideChangeTotals: function(_patchChange) {
+ return (_patchChange.inserted === 0 && _patchChange.deleted === 0);
+ },
+
_computeFileSelected: function(index, selectedIndex) {
return index === selectedIndex;
},
@@ -437,10 +486,6 @@
return expanded ? 'â–¼' : 'â—€';
},
- _computeHiddenState: function(expanded) {
- return !expanded;
- },
-
_computeFilesShown: function(numFilesShown, files) {
return files.base.slice(0, numFilesShown);
},
@@ -478,5 +523,32 @@
_showAllFiles: function() {
this._numFilesShown = this._files.length;
},
+
+ /**
+ * _getDiffViewMode: Get the diff view (side-by-side or unified) based on
+ * the current state.
+ *
+ * The expected behavior is to use the mode specified in the user's
+ * preferences unless they have manually chosen the alternative view. If the
+ * user navigates up to the change view, it should clear this choice and
+ * revert to the preference the next time a diff is viewed.
+ *
+ * Use side-by-side if the user is not logged in.
+ *
+ * @return {String}
+ */
+ _getDiffViewMode: function() {
+ if (this.diffViewMode) {
+ return this.diffViewMode;
+ } else if (this._userPrefs && this._userPrefs.diff_view) {
+ return this.diffViewMode = this._userPrefs.diff_view;
+ }
+
+ return DiffViewMode.SIDE_BY_SIDE;
+ },
+
+ _handleDropdownChange: function(e) {
+ e.target.blur();
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 502597c..b298fef 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -32,20 +32,36 @@
</template>
</test-fixture>
+<test-fixture id="blank">
+ <template>
+ <div></div>
+ </template>
+</test-fixture>
+
<script>
suite('gr-file-list tests', function() {
var element;
+ var sandbox;
setup(function() {
+ sandbox = sinon.sandbox.create();
stub('gr-rest-api-interface', {
getLoggedIn: function() { return Promise.resolve(true); },
getPreferences: function() { return Promise.resolve({}); },
+ fetchJSON: function() { return Promise.resolve({}); },
+ });
+ stub('gr-date-formatter', {
+ _loadTimeFormat: function() { return Promise.resolve(''); }
});
element = fixture('basic');
});
+ teardown(function() {
+ sandbox.restore();
+ });
+
test('get file list', function(done) {
- var getChangeFilesStub = sinon.stub(element.$.restAPI, 'getChangeFiles',
+ var getChangeFilesStub = sandbox.stub(element.$.restAPI, 'getChangeFiles',
function() {
return Promise.resolve({
'/COMMIT_MSG': {lines_inserted: 9},
@@ -81,6 +97,37 @@
});
});
+ test('calculate totals for patch number', function() {
+ element._files = [
+ {__path: '/COMMIT_MSG', lines_inserted: 9},
+ {__path: 'file_added_in_rev2.txt', lines_inserted: 1, lines_deleted: 1},
+ {__path: 'myfile.txt', lines_inserted: 1, lines_deleted: 1},
+ ];
+ assert.deepEqual(element._patchChange, {inserted: 2, deleted: 2});
+
+ // Test with a commit message that isn't the first file.
+ element._files = [
+ {__path: 'file_added_in_rev2.txt', lines_inserted: 1, lines_deleted: 1},
+ {__path: '/COMMIT_MSG', lines_inserted: 9},
+ {__path: 'myfile.txt', lines_inserted: 1, lines_deleted: 1},
+ ];
+ assert.deepEqual(element._patchChange, {inserted: 2, deleted: 2});
+
+ // Test with no commit message.
+ element._files = [
+ {__path: 'file_added_in_rev2.txt', lines_inserted: 1, lines_deleted: 1},
+ {__path: 'myfile.txt', lines_inserted: 1, lines_deleted: 1},
+ ];
+ assert.deepEqual(element._patchChange, {inserted: 2, deleted: 2});
+
+ // Test with files missing either lines_inserted or lines_deleted.
+ element._files = [
+ {__path: 'file_added_in_rev2.txt', lines_inserted: 1},
+ {__path: 'myfile.txt', lines_deleted: 1},
+ ];
+ assert.deepEqual(element._patchChange, {inserted: 1, deleted: 1});
+ });
+
suite('keyboard shortcuts', function() {
setup(function() {
element._files = [
@@ -97,19 +144,22 @@
});
test('toggle left diff via shortcut', function() {
- var toggleLeftDiffStub = sinon.stub();
- sinon.stub(element, 'diffs', {get: function() {
+ var toggleLeftDiffStub = sandbox.stub();
+ // Property getter cannot be stubbed w/ sandbox due to a bug in Sinon.
+ // https://github.com/sinonjs/sinon/issues/781
+ var diffsStub = sinon.stub(element, 'diffs', {get: function() {
return [{toggleLeftDiff: toggleLeftDiffStub}];
}});
MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift'); // 'A'
assert.isTrue(toggleLeftDiffStub.calledOnce);
+ diffsStub.restore();
});
test('keyboard shortcuts', function() {
flushAsynchronousOperations();
var elementItems = Polymer.dom(element.root).querySelectorAll(
'.row:not(.header)');
- assert.equal(elementItems.length, 3);
+ assert.equal(elementItems.length, 4);
assert.isTrue(elementItems[0].hasAttribute('selected'));
assert.isFalse(elementItems[1].hasAttribute('selected'));
assert.isFalse(elementItems[2].hasAttribute('selected'));
@@ -117,7 +167,7 @@
assert.equal(element.selectedIndex, 1);
MockInteractions.pressAndReleaseKeyOn(element, 74); // 'J'
- var showStub = sinon.stub(page, 'show');
+ var showStub = sandbox.stub(page, 'show');
assert.equal(element.selectedIndex, 2);
MockInteractions.pressAndReleaseKeyOn(element, 13); // 'ENTER'
assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
@@ -241,7 +291,7 @@
assert.isFalse(fileAdded.checked);
assert.isTrue(myFile.checked);
- var saveStub = sinon.stub(element, '_saveReviewedState',
+ var saveStub = sandbox.stub(element, '_saveReviewedState',
function() { return Promise.resolve(); });
MockInteractions.tap(commitMsg);
@@ -272,7 +322,7 @@
});
test('diff against dropdown', function(done) {
- var showStub = sinon.stub(page, 'show');
+ var showStub = sandbox.stub(page, 'show');
element.changeNum = '42';
element.patchRange = {
basePatchNum: 'PARENT',
@@ -284,7 +334,7 @@
rev3: {_number: 3},
};
flush(function() {
- var selectEl = element.$$('select');
+ var selectEl = element.$.patchChange;
assert.equal(selectEl.value, 'PARENT');
assert.isTrue(element.$$('option[value="3"]').hasAttribute('disabled'));
selectEl.addEventListener('change', function() {
@@ -313,7 +363,7 @@
var fileRows =
Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
// Prevent diff from making API call.
- var diffStub = sinon.stub(element.diffs[0], 'reload');
+ var diffStub = sandbox.stub(element.diffs[0], 'reload');
var showHideCheck = fileRows[0].querySelector(
'input.show-hide[type="checkbox"]');
assert.isTrue(showHideCheck.checked);
@@ -339,5 +389,50 @@
element.$$('a').getAttribute('href'),
'/c/42/2/foo+bar/my%252Bfile.txt%2525');
});
+
+ test('diff mode correctly toggles the diffs', function() {
+ element._files = [
+ {__path: 'myfile.txt', __expanded: false},
+ ];
+ element.changeNum = '42';
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '2',
+ };
+ element.selectedIndex = 0;
+ flushAsynchronousOperations();
+ var select = element.$.modeSelect;
+ var diffDisplay = element.diffs[0];
+ element._userPrefs = {diff_view: 'SIDE_BY_SIDE'};
+ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE');
+ assert.equal(element.diffViewMode, 'SIDE_BY_SIDE');
+ assert.equal(diffDisplay.viewMode, 'SIDE_BY_SIDE');
+ element.set('diffViewMode', 'UNIFIED_DIFF');
+ assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF');
+ assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF');
+ });
+
+ test('diff mode selector initializes from preferences', function() {
+ var resolvePrefs;
+ var prefsPromise = new Promise(function(resolve) {
+ resolvePrefs = resolve;
+ });
+ sandbox.stub(element, '_getPreferences').returns(prefsPromise);
+
+ // Attach a new gr-file-list so we can intercept the preferences fetch.
+ var view = document.createElement('gr-file-list');
+ var select = view.$.modeSelect;
+ fixture('blank').appendChild(view);
+ flushAsynchronousOperations();
+
+ // At this point the diff mode doesn't yet have the user's preference.
+ assert.equal(select.value, 'SIDE_BY_SIDE');
+
+ // Receive the overriding preference.
+ resolvePrefs({diff_view: 'UNIFIED'});
+ flushAsynchronousOperations();
+ assert.equal(select.value, 'SIDE_BY_SIDE');
+ document.getElementById('blank').restore();
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index 1432d8c..cb6a6f9 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -208,6 +208,12 @@
</iron-autogrow-textarea>
</section>
<section class="labelsContainer">
+ <template is="dom-if" if="[[_isClosed(change)]]" id="labelDisabled">
+ <div class="labelDisabledMessage">
+ Setting labels are disabled for this change because it has been
+ closed.
+ </div>
+ </template>
<template is="dom-repeat"
items="[[_computeLabelArray(permittedLabels)]]" as="label">
<div class="labelContainer">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index b3d3849..a61de4d 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -23,6 +23,8 @@
REVIEWERS: 'reviewers',
};
+ var CLOSED_CHANGE_STATUSES = ['ABANDONED', 'MERGED'];
+
Polymer({
is: 'gr-reply-dialog',
@@ -271,6 +273,10 @@
}.bind(this));
},
+ _isClosed: function(change) {
+ return CLOSED_CHANGE_STATUSES.indexOf(change.status) !== -1;
+ },
+
_computeHideDraftList: function(drafts) {
return Object.keys(drafts || {}).length == 0;
},
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 01ca076..6c77afb 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -244,6 +244,18 @@
}).then(done);
});
+ test('message disabled dialogue appears for closed change', function() {
+ element.change = {status: 'ABANDONED'};
+ flushAsynchronousOperations();
+ assert.isOk(element.$$('.labelDisabledMessage'));
+ });
+
+ test('message disabled dialogue does not appear for open change',
+ function() {
+ element.change = {status: 'NEW'};
+ assert.isNotOk(element.$$('.labelDisabledMessage'));
+ });
+
test('_getStorageLocation', function() {
var actual = element._getStorageLocation();
assert.equal(actual.changeNum, changeNum);
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 7a9c4f9..a38152a 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -18,6 +18,7 @@
var CHECK_SIGN_IN_INTERVAL_MS = 60000;
var SIGN_IN_WIDTH_PX = 690;
var SIGN_IN_HEIGHT_PX = 500;
+ var TOO_MANY_FILES = 'too many files to find conflicts';
Polymer({
is: 'gr-error-manager',
@@ -38,6 +39,10 @@
this.unlisten(document, 'network-error', '_handleNetworkError');
},
+ _shouldSupressError: function(msg) {
+ return msg.indexOf(TOO_MANY_FILES) > -1;
+ },
+
_handleServerError: function(e) {
if (e.detail.response.status === 403) {
this._getLoggedIn().then(function(loggedIn) {
@@ -49,7 +54,9 @@
}.bind(this));
} else {
e.detail.response.text().then(function(text) {
- this._showAlert('Server error: ' + text);
+ if (!this._shouldSupressError(text)) {
+ this._showAlert('Server error: ' + text);
+ }
}.bind(this));
}
},
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index f633a7e..44cbde0 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -70,6 +70,20 @@
});
});
+ test('suppress TOO_MANY_FILES error', function(done) {
+ var showAlertStub = sandbox.stub(element, '_showAlert');
+ var textSpy = sandbox.spy(function() {
+ return Promise.resolve('too many files to find conflicts');
+ });
+ element.fire('server-error', {response: {status: 500, text: textSpy}});
+
+ assert.isTrue(textSpy.called);
+ textSpy.lastCall.returnValue.then(function() {
+ assert.isFalse(showAlertStub.called);
+ done();
+ });
+ });
+
test('show network error', function(done) {
var consoleErrorStub = sandbox.stub(console, 'error');
var showAlertStub = sandbox.stub(element, '_showAlert');
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 6e88267..7a7e761 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -98,13 +98,6 @@
<td><span class="key">u</span></td>
<td>Up to change list</td>
</tr>
- <tr>
- <td>
- <span class="key modifier">Shift</span>
- <span class="key">i</span>
- </td>
- <td>Show/hide inline diffs</td>
- </tr>
</tbody>
<!-- Diff View -->
<tbody hidden$="[[!_computeInView(view, 'gr-diff-view')]]" hidden>
@@ -177,6 +170,13 @@
<td>Show selected file</td>
</tr>
<tr>
+ <td>
+ <span class="key modifier">Shift</span>
+ <span class="key">i</span>
+ </td>
+ <td>Show/hide all inline diffs</td>
+ </tr>
+ <tr>
<td><span class="key">i</span></td>
<td>Show/hide selected inline diff</td>
</tr>
@@ -214,6 +214,17 @@
<td>Go to previous comment thread</td>
</tr>
<tr>
+ <td><span class="key">e</span></td>
+ <td>Expand all comment threads</td>
+ </tr>
+ <tr>
+ <td>
+ <span class="key modifier">Shift</span>
+ <span class="key">e</span>
+ </td>
+ <td>Collapse all comment threads</td>
+ </tr>
+ <tr>
<td>
<span class="key modifier">Shift</span>
<span class="key">←</span>
@@ -277,6 +288,17 @@
<td>Show previous comment thread</td>
</tr>
<tr>
+ <td><span class="key">e</span></td>
+ <td>Expand all comment threads</td>
+ </tr>
+ <tr>
+ <td>
+ <span class="key modifier">Shift</span>
+ <span class="key">e</span>
+ </td>
+ <td>Collapse all comment threads</td>
+ </tr>
+ <tr>
<td>
<span class="key modifier">Shift</span>
<span class="key">←</span>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
index 7653655..d10567c 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
@@ -15,6 +15,7 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
<dom-module id="gr-reporting">
<script src="gr-reporting.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 5236a1c..9c0e902 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -33,6 +33,8 @@
var CHANGE_VIEW_REGEX = /^\/c\/\d+\/?\d*$/;
var DIFF_VIEW_REGEX = /^\/c\/\d+\/\d+\/.+$/;
+ var pending = [];
+
Polymer({
is: 'gr-reporting',
@@ -40,7 +42,7 @@
_baselines: {
type: Array,
value: function() { return {}; },
- }
+ },
},
get performanceTiming() {
@@ -51,8 +53,13 @@
return Math.round(10 * window.performance.now()) / 10;
},
- reporter: function(type, category, eventName, eventValue) {
- eventValue = eventValue;
+ reporter: function() {
+ var report = (Gerrit._arePluginsLoaded() && !pending.length) ?
+ this.defaultReporter : this.cachingReporter;
+ report.apply(this, arguments);
+ },
+
+ defaultReporter: function(type, category, eventName, eventValue) {
var detail = {
type: type,
category: category,
@@ -63,6 +70,19 @@
console.log(eventName + ': ' + eventValue);
},
+ cachingReporter: function(type, category, eventName, eventValue) {
+ if (Gerrit._arePluginsLoaded()) {
+ if (pending.length) {
+ pending.splice(0).forEach(function(args) {
+ this.reporter.apply(this, args);
+ }, this);
+ }
+ this.reporter(type, category, eventName, eventValue);
+ } else {
+ pending.push([type, category, eventName, eventValue]);
+ }
+ },
+
/**
* User-perceived app start time, should be reported when the app is ready.
*/
@@ -91,7 +111,7 @@
locationChanged: function() {
var page = '';
var pathname = this._getPathname();
- if (pathname.startsWith('/q/')) {
+ if (pathname.indexOf('/q/') === 0) {
page = '/q/';
} else if (pathname.match(CHANGE_VIEW_REGEX)) { // change view
page = '/c/';
@@ -105,6 +125,10 @@
NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page);
},
+ pluginsLoaded: function() {
+ this.timeEnd('PluginsLoaded');
+ },
+
_getPathname: function() {
return window.location.pathname;
},
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index b9d07fc..082f81b 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -90,6 +90,48 @@
));
});
+ suite('plugins', function() {
+ setup(function() {
+ element.reporter.restore();
+ sandbox.stub(element, 'defaultReporter');
+ sandbox.stub(Gerrit, '_arePluginsLoaded');
+ });
+
+ test('pluginsLoaded reports time', function() {
+ Gerrit._arePluginsLoaded.returns(true);
+ var nowStub = sinon.stub(element, 'now').returns(42);
+ element.pluginsLoaded();
+ assert.isTrue(element.defaultReporter.calledWithExactly(
+ 'timing-report', 'UI Latency', 'PluginsLoaded', 42
+ ));
+ });
+
+ test('caches reports if plugins are not loaded', function() {
+ Gerrit._arePluginsLoaded.returns(false);
+ element.timeEnd('foo');
+ assert.isFalse(element.defaultReporter.called);
+ });
+
+ test('reports if plugins are loaded', function() {
+ Gerrit._arePluginsLoaded.returns(true);
+ element.timeEnd('foo');
+ assert.isTrue(element.defaultReporter.called);
+ });
+
+ test('reports cached events preserving order', function() {
+ Gerrit._arePluginsLoaded.returns(false);
+ element.timeEnd('foo');
+ Gerrit._arePluginsLoaded.returns(true);
+ element.timeEnd('bar');
+ assert.isTrue(element.defaultReporter.firstCall.calledWith(
+ 'timing-report', 'UI Latency', 'foo'
+ ));
+ assert.isTrue(element.defaultReporter.secondCall.calledWith(
+ 'timing-report', 'UI Latency', 'bar'
+ ));
+ });
+ });
+
suite('location changed', function() {
var pathnameStub;
setup(function() {
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 071050c..05b191c 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -59,6 +59,11 @@
}
// For backward compatibility with GWT links.
if (data.hash) {
+ // In certain login flows the server may redirect to a hash without
+ // a leading slash, which page.js doesn't handle correctly.
+ if (data.hash[0] !== '/') {
+ data.hash = '/' + data.hash
+ }
page.redirect(data.hash);
return;
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index 40a90b7..3ad7c74 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -58,10 +58,20 @@
SYNTAX: 'Diff Syntax Render',
};
+ // If any line of the diff is more than the character limit, then disable
+ // syntax highlighting for the entire file.
+ var SYNTAX_MAX_LINE_LENGTH = 500;
+
Polymer({
is: 'gr-diff-builder',
/**
+ * Fired when the diff begins rendering.
+ *
+ * @event render-start
+ */
+
+ /**
* Fired when the diff is rendered.
*
* @event render
@@ -121,10 +131,16 @@
reporting.time(TimingLabel.TOTAL);
reporting.time(TimingLabel.CONTENT);
+ this.fire('render-start');
return this.$.processor.process(this.diff.content).then(function() {
if (this.isImageDiff) {
this._builder.renderDiffImages();
}
+
+ if (this._anyLineTooLong()) {
+ this.$.syntaxLayer.enabled = false;
+ }
+
reporting.timeEnd(TimingLabel.CONTENT);
reporting.time(TimingLabel.SYNTAX);
this.$.syntaxLayer.process().then(function() {
@@ -376,6 +392,18 @@
Polymer.dom.flush();
parent.removeChild(thread);
},
+
+ /**
+ * @returns {Boolean} whether any of the lines in _groups are longer
+ * than SYNTAX_MAX_LINE_LENGTH.
+ */
+ _anyLineTooLong: function() {
+ return this._groups.reduce(function(acc, group) {
+ return acc || group.lines.reduce(function(acc, line) {
+ return acc || line.text.length >= SYNTAX_MAX_LINE_LENGTH;
+ }, false);
+ }, false);
+ },
});
})();
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
index af44629..341e959 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -613,6 +613,31 @@
assert.strictEqual(sections[0], section[0]);
assert.strictEqual(sections[1], section[1]);
});
+
+ test('render-start and render are fired', function() {
+ var fireStub = sinon.stub(element, 'fire');
+ element.render({left: [], right: []}, {});
+ flush(function() {
+ assert.isTrue(fireStub.calledWithExactly('render-start'));
+ assert.isTrue(fireStub.calledWithExactly('render'));
+ done();
+ });
+ });
+
+ test('rendering normal-sized diff does not disable syntax', function() {
+ flush(function() {
+ assert.isTrue(element.$.syntaxLayer.enabled);
+ });
+ });
+
+ test('rendering large diff disables syntax', function() {
+ // Before it renders, set the first diff line to 500 '*' characters.
+ element.diff.content[0].a = new Array(501).join('*');
+
+ flush(function() {
+ assert.isFalse(element.$.syntaxLayer.enabled);
+ });
+ });
});
suite('mock-diff', function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
index 305c36a..963084a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -29,6 +29,10 @@
type: Array,
value: function() { return []; },
},
+ keyEventTarget: {
+ type: Object,
+ value: function() { return document.body; },
+ },
patchNum: String,
path: String,
projectConfig: Object,
@@ -41,6 +45,10 @@
_orderedComments: Array,
},
+ behaviors: [
+ Gerrit.KeyboardShortcutBehavior,
+ ],
+
listeners: {
'comment-update': '_handleCommentUpdate',
},
@@ -80,6 +88,22 @@
this._orderedComments = this._sortedComments(this.comments);
},
+ _handleKey: function(e) {
+ if (this.shouldSupressKeyboardShortcut(e)) { return; }
+ if (e.keyCode === 69) { // 'e'
+ e.preventDefault();
+ this._expandCollapseComments(e.shiftKey);
+ }
+ },
+
+ _expandCollapseComments: function(actionIsCollapse) {
+ var comments =
+ Polymer.dom(this.root).querySelectorAll('gr-diff-comment');
+ comments.forEach(function(comment) {
+ comment.collapsed = actionIsCollapse;
+ });
+ },
+
_sortedComments: function(comments) {
comments.sort(function(c1, c2) {
var c1Date = c1.__date || util.parseDate(c1.updated);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
index 641dc0f..eb87f56 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -54,30 +54,25 @@
message: 'i like you, too',
in_reply_to: 'sallys_confession',
updated: '2015-12-25 15:00:20.396000000',
- },
- {
+ }, {
id: 'sallys_confession',
message: 'i like you, jack',
updated: '2015-12-24 15:00:20.396000000',
- },
- {
+ }, {
id: 'sally_to_dr_finklestein',
message: 'i’m running away',
updated: '2015-10-31 09:00:20.396000000',
- },
- {
+ }, {
id: 'sallys_defiance',
in_reply_to: 'sally_to_dr_finklestein',
message: 'i will poison you so i can get away',
updated: '2015-10-31 15:00:20.396000000',
- },
- {
+ }, {
id: 'dr_finklesteins_response',
in_reply_to: 'sally_to_dr_finklestein',
message: 'no i will pull a thread and your arm will fall off',
updated: '2015-10-31 11:00:20.396000000'
- },
- {
+ }, {
id: 'sallys_mission',
message: 'i have to find santa',
updated: '2015-12-24 21:00:20.396000000'
@@ -89,31 +84,26 @@
id: 'sally_to_dr_finklestein',
message: 'i’m running away',
updated: '2015-10-31 09:00:20.396000000',
- },
- {
+ }, {
id: 'dr_finklesteins_response',
in_reply_to: 'sally_to_dr_finklestein',
message: 'no i will pull a thread and your arm will fall off',
updated: '2015-10-31 11:00:20.396000000'
- },
- {
+ }, {
id: 'sallys_defiance',
in_reply_to: 'sally_to_dr_finklestein',
message: 'i will poison you so i can get away',
updated: '2015-10-31 15:00:20.396000000',
- },
- {
+ }, {
id: 'sallys_confession',
message: 'i like you, jack',
updated: '2015-12-24 15:00:20.396000000',
- },
- {
+ }, {
id: 'jacks_reply',
message: 'i like you, too',
in_reply_to: 'sallys_confession',
updated: '2015-12-25 15:00:20.396000000',
- },
- {
+ }, {
id: 'sallys_mission',
message: 'i have to find santa',
updated: '2015-12-24 21:00:20.396000000'
@@ -247,20 +237,17 @@
message: 'i like you, too',
in_reply_to: 'sallys_confession',
updated: '2015-12-25 15:00:20.396000000',
- },
- {
+ }, {
id: 'sallys_confession',
in_reply_to: 'nonexistent_comment',
message: 'i like you, jack',
updated: '2015-12-24 15:00:20.396000000',
- },
- {
+ }, {
id: 'sally_to_dr_finklestein',
in_reply_to: 'nonexistent_comment',
message: 'i’m running away',
updated: '2015-10-31 09:00:20.396000000',
- },
- {
+ }, {
id: 'sallys_defiance',
message: 'i will poison you so i can get away',
updated: '2015-10-31 15:00:20.396000000',
@@ -268,5 +255,37 @@
element.comments = comments;
assert.equal(4, element._orderedComments.length);
});
+
+ test('keyboard shortcuts', function() {
+ var comments = [
+ {
+ id: 'jacks_reply',
+ message: 'i like you, too',
+ in_reply_to: 'sallys_confession',
+ updated: '2015-12-25 15:00:20.396000000',
+ }, {
+ id: 'sallys_confession',
+ in_reply_to: 'nonexistent_comment',
+ message: 'i like you, jack',
+ updated: '2015-12-24 15:00:20.396000000',
+ }, {
+ id: 'sally_to_dr_finklestein',
+ in_reply_to: 'nonexistent_comment',
+ message: 'i’m running away',
+ updated: '2015-10-31 09:00:20.396000000',
+ }, {
+ id: 'sallys_defiance',
+ message: 'i will poison you so i can get away',
+ updated: '2015-10-31 15:00:20.396000000',
+ }];
+ element.comments = comments;
+ var expandCollapseStub = sinon.stub(element, '_expandCollapseComments');
+ MockInteractions.pressAndReleaseKeyOn(element, 69); // 'e'
+ assert.isTrue(expandCollapseStub.lastCall.calledWith(false));
+
+ MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift'); // 'e'
+ assert.isTrue(expandCollapseStub.lastCall.calledWith(true));
+ expandCollapseStub.restore();
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index c3b6233..ebb1f87 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -44,15 +44,20 @@
padding: .5em .7em;
}
.header {
+ cursor: pointer;
display: flex;
- padding-bottom: 0;
font-family: 'Open Sans', sans-serif;
+ padding-bottom: 0;
}
- .headerLeft {
+ .headerMiddle {
+ color: #666;
flex: 1;
+ overflow: hidden;
}
.authorName,
.draftLabel {
+ display: block;
+ float: left;
font-weight: bold;
}
.draftLabel {
@@ -62,6 +67,7 @@
.date {
justify-content: flex-end;
margin-left: 5px;
+ white-space: nowrap;
}
a.date:link,
a.date:visited {
@@ -113,19 +119,62 @@
background-color: #fff;
display: block;
}
+ .show-hide {
+ margin-left: .4em;
+ }
+ input.show-hide {
+ display: none;
+ }
+ label.show-hide {
+ color: #000;
+ cursor: pointer;
+ display: block;
+ font-size: .8em;
+ height: 1.1em;
+ margin-top: .1em;
+ }
+ #container .collapsedContent {
+ display: none;
+ }
+ #container.collapsed {
+ padding-bottom: 3px;
+ }
+ #container.collapsed .collapsedContent {
+ display: block;
+ overflow: hidden;
+ padding-left: 5px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ #container.collapsed .actions,
+ #container.collapsed gr-linked-text,
+ #container.collapsed iron-autogrow-textarea {
+ display: none;
+ }
</style>
<div id="container"
class="container"
on-mouseenter="_handleMouseEnter"
on-mouseleave="_handleMouseLeave">
- <div class="header" id="header">
+ <div class="header" id="header" on-click="_handleToggleCollapsed">
<div class="headerLeft">
<span class="authorName">[[comment.author.name]]</span>
<span class="draftLabel">DRAFT</span>
</div>
+ <div class="headerMiddle">
+ <span class="collapsedContent">[[comment.message]]</span>
+ </div>
<a class="date" href$="[[_computeLinkToComment(comment)]]" on-tap="_handleLinkTap">
<gr-date-formatter date-str="[[comment.updated]]"></gr-date-formatter>
</a>
+ <div class="show-hide">
+ <label class="show-hide">
+ <input type="checkbox" class="show-hide"
+ checked$="[[collapsed]]"
+ on-change="_handleToggleCollapsed">
+ [[_computeShowHideText(collapsed)]]
+ </label>
+ </div>
</div>
<iron-autogrow-textarea
id="editTextarea"
@@ -137,6 +186,7 @@
<gr-linked-text class="message"
pre
content="[[comment.message]]"
+ collapsed="[[collapsed]]"
config="[[projectConfig.commentlinks]]"></gr-linked-text>
<div class="actions" hidden$="[[!showActions]]">
<gr-button class="action reply" on-tap="_handleReply">Reply</gr-button>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index 07badbf..2abd0a8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -81,6 +81,11 @@
},
patchNum: String,
showActions: Boolean,
+ collapsed: {
+ type: Boolean,
+ value: true,
+ observer: '_toggleCollapseClass',
+ },
projectConfig: Object,
_xhrPromise: Object, // Used for testing.
@@ -96,10 +101,20 @@
'_loadLocalDraft(changeNum, patchNum, comment)',
],
+ attached: function() {
+ if (this.editing) {
+ this.collapsed = false;
+ }
+ },
+
detached: function() {
this.cancelDebouncer('fire-update');
},
+ _computeShowHideText: function(collapsed) {
+ return collapsed ? 'â—€' : 'â–¼';
+ },
+
save: function() {
this.comment.message = this._messageText;
this.disabled = true;
@@ -210,6 +225,18 @@
}
},
+ _handleToggleCollapsed: function() {
+ this.collapsed = !this.collapsed;
+ },
+
+ _toggleCollapseClass: function(collapsed) {
+ if (collapsed) {
+ this.$.container.classList.add('collapsed');
+ } else {
+ this.$.container.classList.remove('collapsed');
+ }
+ },
+
_commentMessageChanged: function(message) {
this._messageText = message || '';
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index bddc3ab..0a591ae 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -39,6 +39,12 @@
</test-fixture>
<script>
+
+ function isVisible(el) {
+ assert.ok(el);
+ return getComputedStyle(el).getPropertyValue('display') !== 'none';
+ }
+
suite('gr-diff-comment tests', function() {
var element;
setup(function() {
@@ -58,6 +64,34 @@
};
});
+ test('collapsible comments', function() {
+ // When a comment (not draft) is loaded, it should be collapsed
+ assert.isTrue(element.collapsed);
+ assert.isFalse(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is not visible');
+ assert.isFalse(isVisible(element.$$('.actions')),
+ 'actions are not visible');
+ assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is not visible');
+
+ // The header middle content is only visible when comments are collapsed.
+ // It shows the message in a condensed way, and limits to a single line.
+ assert.isTrue(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is visible');
+
+ // When the header row is clicked, the comment should expand
+ MockInteractions.tap(element.$.header);
+ assert.isFalse(element.collapsed);
+ assert.isTrue(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is visible');
+ assert.isTrue(isVisible(element.$$('.actions')),
+ 'actions are visible');
+ assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is not visible');
+ assert.isFalse(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is not visible');
+ });
+
test('proper event fires on reply', function(done) {
element.addEventListener('reply', function(e) {
assert.ok(e.detail.comment);
@@ -92,6 +126,29 @@
'Should navigate to ' + dest + ' without triggering nav');
showStub.restore();
});
+
+ test('comment expand and collapse', function() {
+ element.collapsed = true;
+ assert.isFalse(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is not visible');
+ assert.isFalse(isVisible(element.$$('.actions')),
+ 'actions are not visible');
+ assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is not visible');
+ assert.isTrue(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is visible');
+
+ element.collapsed = false;
+ assert.isFalse(element.collapsed);
+ assert.isTrue(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is visible');
+ assert.isTrue(isVisible(element.$$('.actions')),
+ 'actions are visible');
+ assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is not visible');
+ assert.isFalse(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is is not visible');
+ });
});
suite('gr-diff-comment draft tests', function() {
@@ -135,11 +192,6 @@
};
});
- function isVisible(el) {
- assert.ok(el);
- return getComputedStyle(el).getPropertyValue('display') != 'none';
- }
-
test('button visibility states', function() {
element.showActions = false;
assert.isTrue(element.$$('.actions').hasAttribute('hidden'));
@@ -181,6 +233,71 @@
assert.isTrue(isVisible(element.$$('.cancel')), 'cancel is visible');
});
+ test('collapsible drafts', function() {
+ element.addEventListener('reply', function(e) {
+ assert.ok(e.detail.comment);
+ done();
+ });
+ assert.isTrue(element.collapsed);
+ assert.isFalse(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is not visible');
+ assert.isFalse(isVisible(element.$$('.actions')),
+ 'actions are not visible');
+ assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is not visible');
+ assert.isTrue(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is visible');
+
+ MockInteractions.tap(element.$.header);
+ assert.isFalse(element.collapsed);
+ assert.isTrue(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is visible');
+ assert.isTrue(isVisible(element.$$('.actions')),
+ 'actions are visible');
+ assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is not visible');
+ assert.isFalse(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is is not visible');
+
+ // When the edit button is pressed, should still see the actions
+ // and also textarea
+ MockInteractions.tap(element.$$('.edit'));
+ assert.isFalse(element.collapsed);
+ assert.isFalse(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is not visible');
+ assert.isTrue(isVisible(element.$$('.actions')),
+ 'actions are visible');
+ assert.isTrue(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is visible');
+ assert.isFalse(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is not visible');
+
+ // When toggle again, everything should be hidden except for textarea
+ // and header middle content should be visible
+ MockInteractions.tap(element.$.header);
+ assert.isTrue(element.collapsed);
+ assert.isFalse(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is not visible');
+ assert.isFalse(isVisible(element.$$('.actions')),
+ 'actions are not visible');
+ assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is not visible');
+ assert.isTrue(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is visible');
+
+ // When toggle again, textarea should remain open in the state it was
+ // before
+ MockInteractions.tap(element.$.header);
+ assert.isFalse(isVisible(element.$$('gr-linked-text')),
+ 'gr-linked-text is not visible');
+ assert.isTrue(isVisible(element.$$('.actions')),
+ 'actions are visible');
+ assert.isTrue(isVisible(element.$$('iron-autogrow-textarea')),
+ 'textarea is visible');
+ assert.isFalse(isVisible(element.$$('.collapsedContent')),
+ 'header middle content is not visible');
+ });
+
test('draft creation/cancelation', function(done) {
assert.isFalse(element.editing);
MockInteractions.tap(element.$$('.edit'));
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
index 491eded..c8eea3c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
@@ -21,7 +21,7 @@
<template>
<gr-cursor-manager
id="cursorManager"
- scroll="keep-visible"
+ scroll="[[_scrollBehavior]]"
cursor-target-class="target-row"
target="{{diffRow}}"></gr-cursor-manager>
</template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index dd11f2c..e783658 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -24,6 +24,11 @@
UNIFIED: 'UNIFIED_DIFF',
};
+ var ScrollBehavior = {
+ KEEP_VISIBLE: 'keep-visible',
+ NEVER: 'never',
+ };
+
var LEFT_SIDE_CLASS = 'target-side-left';
var RIGHT_SIDE_CLASS = 'target-side-right';
@@ -63,6 +68,18 @@
type: Number,
value: null,
},
+
+ /**
+ * The scroll behavior for the cursor. Values are 'never' and
+ * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+ * the viewport.
+ */
+ _scrollBehavior: {
+ type: String,
+ value: ScrollBehavior.KEEP_VISIBLE,
+ },
+
+ _listeningForScroll: Boolean,
},
observers: [
@@ -70,6 +87,15 @@
'_diffsChanged(diffs.splices)',
],
+ attached: function() {
+ // Catch when users are scrolling as the view loads.
+ this.listen(window, 'scroll', '_handleWindowScroll');
+ },
+
+ detached: function() {
+ this.unlisten(window, 'scroll', '_handleWindowScroll');
+ },
+
moveLeft: function() {
this.side = DiffSides.LEFT;
if (this._isTargetBlank()) {
@@ -169,12 +195,25 @@
}
},
+ _handleWindowScroll: function() {
+ if (this._listeningForScroll) {
+ this._scrollBehavior = ScrollBehavior.NEVER;
+ this._listeningForScroll = false;
+ }
+ },
+
handleDiffUpdate: function() {
this._updateStops();
if (!this.diffRow) {
this.reInitCursor();
}
+ this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE;
+ this._listeningForScroll = false;
+ },
+
+ _handleDiffRenderStart: function() {
+ this._listeningForScroll = true;
},
/**
@@ -320,12 +359,15 @@
for (i = splice.index;
i < splice.index + splice.addedCount;
i++) {
+ this.listen(this.diffs[i], 'render-start', '_handleDiffRenderStart');
this.listen(this.diffs[i], 'render', 'handleDiffUpdate');
}
for (i = 0;
i < splice.removed && splice.removed.length;
i++) {
+ this.unlisten(splice.removed[i],
+ 'render-start', '_handleDiffRenderStart');
this.unlisten(splice.removed[i], 'render', 'handleDiffUpdate');
}
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index 5bdd138..a7d98e6 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -98,6 +98,17 @@
assert.equal(cursorElement.diffRow, firstDeltaRow);
});
+ test('cursor scroll behavior', function() {
+ cursorElement._handleDiffRenderStart();
+ assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+
+ cursorElement._handleWindowScroll();
+ assert.equal(cursorElement._scrollBehavior, 'never');
+
+ cursorElement.handleDiffUpdate();
+ assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+ });
+
suite('unified diff', function() {
setup(function(done) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index 2dd4c91..d3cc461 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -86,6 +86,7 @@
},
detached: function() {
+ this.cancel();
this.unlisten(window, 'scroll', '_handleWindowScroll');
},
@@ -257,7 +258,7 @@
}
// If there is a range to hide.
- if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 0) {
+ if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 1) {
var linesBeforeCtx = lines.slice(0, hiddenRange[0]);
var hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]);
var linesAfterCtx = lines.slice(hiddenRange[1]);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 4f8c532..f6d0e37 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -511,6 +511,17 @@
assert.equal(result[0].lines.length, rows.length);
});
+ test('_sharedGroupsFromRows no single line collapse', function() {
+ rows = rows.slice(0, 7);
+ var context = 3;
+ var result = element._sharedGroupsFromRows(
+ rows, context, 10, 100);
+
+ // Results in one uncollapsed group with all rows.
+ assert.equal(result.length, 1, 'Results in one group');
+ assert.equal(result[0].lines.length, rows.length);
+ });
+
test('_deltaLinesFromRows', function() {
var startLineNum = 10;
var result = element._deltaLinesFromRows(GrDiffLine.Type.ADD, rows,
@@ -591,5 +602,12 @@
});
});
});
+
+ test('detaching cancels', function() {
+ element = fixture('basic');
+ sandbox.stub(element, 'cancel');
+ element.detached();
+ assert(element.cancel.called);
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 6c73e08..ae5dfd2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -16,6 +16,8 @@
var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
+ var MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 900;
+
var DiffViewMode = {
SIDE_BY_SIDE: 'SIDE_BY_SIDE',
UNIFIED: 'UNIFIED_DIFF',
@@ -104,14 +106,18 @@
this._setReviewed(true);
}
}.bind(this));
-
if (this.changeViewState.diffMode === null) {
- // Initialize with user's diff mode preference. Default to
- // SIDE_BY_SIDE in the meantime.
- this.set('changeViewState.diffMode', DiffViewMode.SIDE_BY_SIDE);
- this.$.restAPI.getPreferences().then(function(prefs) {
- this.set('changeViewState.diffMode', prefs.diff_view);
- }.bind(this));
+ // If screen size is small, always default to unified view.
+ if (this._getWindowWidth() < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX) {
+ this.set('changeViewState.diffMode', DiffViewMode.UNIFIED);
+ } else {
+ // Initialize with user's diff mode preference. Default to
+ // SIDE_BY_SIDE in the meantime.
+ this.set('changeViewState.diffMode', DiffViewMode.SIDE_BY_SIDE);
+ this.$.restAPI.getPreferences().then(function(prefs) {
+ this.set('changeViewState.diffMode', prefs.diff_view);
+ }.bind(this));
+ }
}
if (this._path) {
@@ -156,6 +162,10 @@
return this.$.restAPI.getPreferences();
},
+ _getWindowWidth: function() {
+ return window.innerWidth;
+ },
+
_handleReviewedChange: function(e) {
this._setReviewed(Polymer.dom(e).rootTarget.checked);
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index 1eb3f95..e81bc2f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -457,7 +457,6 @@
// We will simulate a user change of the selected mode.
var newMode = 'UNIFIED_DIFF';
-
// Set the actual value of the select, and simulate the change event.
select.value = newMode;
element.fire('change', {}, {node: select});
@@ -492,6 +491,39 @@
assert.equal(select.value, 'SIDE_BY_SIDE');
});
+ test('unified view is always default on small screens', function() {
+ var resolvePrefs;
+ var prefsPromise = new Promise(function(resolve) {
+ resolvePrefs = resolve;
+ });
+
+ var getPreferencesStub = sinon.stub(element.$.restAPI, 'getPreferences',
+ function() { return prefsPromise; });
+
+ // Attach a new gr-diff-view so we can intercept the preferences fetch.
+ var view = document.createElement('gr-diff-view');
+
+ view.changeViewState = {diffMode: null};
+
+ sinon.stub(view, '_getWindowWidth', function() {
+ return 800;
+ });
+
+ var select = view.$.modeSelect;
+ fixture('blank').appendChild(view);
+ flushAsynchronousOperations();
+
+ // At this point the diff mode doesn't yet have the user's preference.
+ assert.equal(select.value, 'UNIFIED_DIFF');
+
+ // Receive the overriding preference.
+ resolvePrefs({diff_view: 'SIDE_BY_SIDE'});
+ flushAsynchronousOperations();
+
+ // On small screens, unified should override user perferences
+ assert.equal(select.value, 'UNIFIED_DIFF');
+ });
+
test('_loadHash', function() {
assert.isNotOk(element.$.cursor.initialLineNumber);
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 dc818e3..1a6759b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -34,8 +34,9 @@
properties: {
changeNum: String,
- hidden: {
+ expanded: {
type: Boolean,
+ value: true,
observer: '_handleShowDiff',
},
patchRange: Object,
@@ -92,14 +93,7 @@
},
ready: function() {
- // Reload only if the diff is not hidden and params are supplied.
- if (this.changeNum && this.patchRange && this.path && !this.hidden) {
- this.reload();
- }
- },
-
- _handleShowDiff: function(hidden) {
- if (!hidden) {
+ if (this._canRender()) {
this.reload();
}
},
@@ -126,7 +120,7 @@
},
getCursorStops: function() {
- if (this.hidden) {
+ if (!this.expanded) {
return [];
}
@@ -159,6 +153,16 @@
this.toggleClass('no-left');
},
+ _handleShowDiff: function() {
+ if (this._canRender()) {
+ this.reload();
+ }
+ },
+
+ _canRender: function() {
+ return this.changeNum && this.patchRange && this.path && this.expanded;
+ },
+
_getCommentThreads: function() {
return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread');
},
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 2f0b6bd..b9b49c0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -468,15 +468,20 @@
assert.equal(drafts[0].id, id);
});
- test('_handleShowDiff reloads when hidden is made false',
+ test('_handleShowDiff reloads when expanded is made true',
function(done) {
+ element.expanded = false;
+ element.changeNum = element._comments.meta.changeNum;
+ element.patchRange = element._comments.meta.patchRange;
+ element.path = element._comments.meta.path;
+
var stub = sinon.stub(element, 'reload', function() {
assert.isTrue(stub.called);
stub.restore();
done();
});
var spy = sinon.spy(element, '_handleShowDiff');
- element.set('hidden', false);
+ element.set('expanded', true);
});
});
});
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
index 9a8ea37..98f6b01 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
@@ -21,26 +21,25 @@
<template>
<style>
:host {
- --gr-arrow-size: .6em;
+ --gr-arrow-size: .65em;
- background-color: #fff;
- border: 1px solid #000;
- border-radius: .5em;
+ background-color: rgba(22, 22, 22, .9);
+ border-radius: 3px;
+ color: #fff;
cursor: pointer;
- padding: .3em;
+ font-family: var(--font-family);
+ padding: .5em .75em;
position: absolute;
white-space: nowrap;
}
.arrow {
- background: #fff;
- border: var(--gr-arrow-size) solid #000;
- border-width: 0 1px 1px 0;
- height: var(--gr-arrow-size);
- left: calc(50% - 1em);
- margin-top: .05em;
+ border: var(--gr-arrow-size) solid transparent;
+ border-top: var(--gr-arrow-size) solid rgba(22, 22, 22, 0.9);
+ height: 0;
+ left: calc(50% - var(--gr-arrow-size));
+ margin-top: .5em;
position: absolute;
- transform: rotate(45deg);
- width: var(--gr-arrow-size);
+ width: 0;
}
</style>
Press <strong>C</strong> to comment.
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index fbd1ef3..29b3c19 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -48,7 +48,7 @@
],
listeners: {
- 'tap': '_handleTap',
+ 'mousedown': '_handleMouseDown', // See https://crbug.com/gerrit/4767
},
placeAbove: function(el) {
@@ -87,7 +87,9 @@
}
},
- _handleTap: function() {
+ _handleMouseDown: function(e) {
+ e.preventDefault();
+ e.stopPropagation();
this._fireCreateComment();
},
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 c0ee1fa..84e97bd 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
@@ -255,13 +255,14 @@
var nodeLength = GrAnnotation.getLength(node);
// Note: HLJS may emit a span with class undefined when it thinks there
// may be a syntax error.
- if (node.tagName === 'SPAN' && node.className !== 'undefined' &&
- CLASS_WHITELIST.hasOwnProperty(node.className)) {
- result.push({
- start: offset,
- length: nodeLength,
- className: node.className,
- });
+ if (node.tagName === 'SPAN' && node.className !== 'undefined') {
+ if (CLASS_WHITELIST.hasOwnProperty(node.className)) {
+ result.push({
+ start: offset,
+ length: nodeLength,
+ className: node.className,
+ });
+ }
if (node.children.length) {
result = result.concat(this._rangesFromElement(node, offset));
}
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index 178f61b..aa37f1a 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -370,6 +370,15 @@
assert.equal(result[1].className, className);
});
+ test('_rangesFromString whitelist allows recursion', function() {
+ var str = [
+ '<span class="non-whtelisted-class">',
+ '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>',
+ '</span>'].join('');
+ var result = element._rangesFromString(str);
+ assert.notEqual(result.length, 0);
+ });
+
test('_isSectionDone', function() {
var state = {sectionIndex: 0, lineIndex: 0};
assert.isFalse(element._isSectionDone(state));
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 9382f3a..633e8f2 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -104,7 +104,8 @@
<gr-change-view
params="[[params]]"
server-config="[[_serverConfig]]"
- view-state="{{_viewState.changeView}}"></gr-change-view>
+ view-state="{{_viewState.changeView}}"
+ back-page="[[_lastSearchPage]]"></gr-change-view>
</template>
<template is="dom-if" if="[[_showDiffView]]" restamp="true">
<gr-diff-view
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 2c7a999..a188ea1 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -43,6 +43,7 @@
_showSettingsView: Boolean,
_viewState: Object,
_lastError: Object,
+ _lastSearchPage: String,
_path: String,
},
@@ -110,10 +111,12 @@
},
_loadPlugins: function(plugins) {
+ Gerrit._setPluginsCount(plugins.length);
for (var i = 0; i < plugins.length; i++) {
var scriptEl = document.createElement('script');
scriptEl.defer = true;
scriptEl.src = '/' + plugins[i];
+ scriptEl.onerror = Gerrit._pluginInstalled;
document.body.appendChild(scriptEl);
}
},
@@ -163,10 +166,18 @@
_handleLocationChange: function(e) {
var hash = e.detail.hash.substring(1);
var pathname = e.detail.pathname;
- if (pathname.startsWith('/c/') && parseInt(hash, 10) > 0) {
+ if (pathname.indexOf('/c/') === 0 && parseInt(hash, 10) > 0) {
pathname += '@' + hash;
}
this.set('_path', pathname);
+ this._handleSearchPageChange();
+ },
+
+ _handleSearchPageChange: function() {
+ var viewsToCheck = ['gr-change-list-view', 'gr-dashboard-view'];
+ if (viewsToCheck.indexOf(this.params.view) !== -1) {
+ this.set('_lastSearchPage', location.pathname);
+ }
},
_handleTitleChange: function(e) {
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index c5c68ea..9373820 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -74,9 +74,18 @@
var event = {detail: curLocation};
var gwtLink = element.$$('#gwtLink');
+
+ sinon.stub(element, '_handleSearchPageChange');
+
element._handleLocationChange(event);
assert.equal(gwtLink.href,
'http://' + location.host + '/?polygerrit=0#/c/1/1/testfile.txt@2');
});
+
+ test('sets plugins count', function() {
+ sandbox.stub(Gerrit, '_setPluginsCount');
+ element._loadPlugins([]);
+ assert.isTrue(Gerrit._setPluginsCount.calledWithExactly(0));
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
index d3585ef..8d89692 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
@@ -36,7 +36,8 @@
<span>
<a href$="[[_computeOwnerLink(account)]]">
<gr-account-label account="[[account]]"
- avatar-image-size="[[avatarImageSize]]"></gr-account-label>
+ avatar-image-size="[[avatarImageSize]]"
+ show-email="[[_computeShowEmail(account)]]"></gr-account-label>
</a>
</span>
</template>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index 058b27d..0c2ad0b 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -30,5 +30,9 @@
var accountID = account.email || account._account_id;
return '/q/owner:' + encodeURIComponent(accountID) + '+status:open';
},
+
+ _computeShowEmail: function(account) {
+ return !!(account && !account.name);
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
index 2b5b831..1a84d15 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -48,6 +48,10 @@
assert.equal(element._computeOwnerLink({_account_id: 42}),
'/q/owner:42+status:open');
+
+ assert.equal(element._computeShowEmail({name: 'asd'}), false);
+
+ assert.equal(element._computeShowEmail({}), true);
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
index f7c337b..72c7f6e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
@@ -33,6 +33,11 @@
});
};
+ GrChangeActionsInterface.prototype.setActionHidden = function(type, key,
+ hidden) {
+ return this._el.setActionHidden(type, key, hidden);
+ };
+
GrChangeActionsInterface.prototype.add = function(type, label) {
return this._el.addActionButton(type, label);
};
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index 4919a5a..93e676c 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -119,5 +119,22 @@
});
});
});
+
+ test('hide action buttons', function(done) {
+ var key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!');
+ flush(function() {
+ var button = element.$$('[data-action-key="' + key + '"]');
+ assert.isOk(button);
+ assert.isFalse(button.hasAttribute('hidden'));
+ changeActions.setActionHidden(changeActions.ActionType.REVISION, key,
+ true);
+ flush(function() {
+ var button = element.$$('[data-action-key="' + key + '"]');
+ assert.isOk(button);
+ assert.isTrue(button.hasAttribute('hidden'));
+ done();
+ });
+ });
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index 1967b80..5c0535b 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -14,6 +14,7 @@
limitations under the License.
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-js-api-interface">
@@ -23,4 +24,3 @@
<script src="gr-js-api-interface.js"></script>
<script src="gr-public-js-api.js"></script>
</dom-module>
-
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
index 06b25de..bb407aa 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -150,15 +150,15 @@
});
},
- modifyRevertMsg: function(change, msg) {
+ modifyRevertMsg: function(change, revertMsg, origMsg) {
this._getEventCallbacks(EventType.REVERT).forEach(function(callback) {
try {
- msg = callback(change, msg);
+ revertMsg = callback(change, revertMsg, origMsg);
} catch (err) {
console.error(err);
}
});
- return msg;
+ return revertMsg;
},
getLabelValuesPostRevert: function(change) {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 97e0a18..766da84 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -103,21 +103,23 @@
});
test('revert event', function(done) {
- function appendToRevertMsg(c, msg) {
- return msg + '\ninfo';
+ function appendToRevertMsg(c, revertMsg, originalMsg) {
+ return revertMsg + '\n' + originalMsg.replace(/^/gm, '> ') + '\ninfo';
}
done();
- assert.equal(element.modifyRevertMsg(null, 'test'), 'test');
+ assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), 'test');
assert.equal(errorStub.callCount, 0);
plugin.on(element.EventType.REVERT, throwErrFn);
plugin.on(element.EventType.REVERT, appendToRevertMsg);
- assert.equal(element.modifyRevertMsg(null, 'test'), 'test\ninfo');
+ assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
+ 'test\n> origTest\ninfo');
assert.isTrue(errorStub.calledOnce);
plugin.on(element.EventType.REVERT, appendToRevertMsg);
- assert.equal(element.modifyRevertMsg(null, 'test'), 'test\ninfo\ninfo');
+ assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'),
+ 'test\n> origTest\ninfo\n> origTest\ninfo');
assert.isTrue(errorStub.calledTwice);
});
@@ -172,5 +174,49 @@
});
});
+ test('_setPluginsCount', function(done) {
+ stub('gr-reporting', {
+ pluginsLoaded: function() {
+ assert.equal(Gerrit._pluginsPending, 0);
+ done();
+ }
+ });
+ Gerrit._setPluginsCount(0);
+ });
+
+ test('_arePluginsLoaded', function() {
+ assert.isFalse(Gerrit._arePluginsLoaded());
+ Gerrit._setPluginsCount(1);
+ assert.isFalse(Gerrit._arePluginsLoaded());
+ Gerrit._setPluginsCount(0);
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ });
+
+ test('_pluginInstalled', function(done) {
+ stub('gr-reporting', {
+ pluginsLoaded: function() {
+ done();
+ }
+ });
+ Gerrit._setPluginsCount(2);
+ Gerrit._pluginInstalled();
+ assert.equal(Gerrit._pluginsPending, 1);
+ Gerrit._pluginInstalled();
+ });
+
+ test('install calls _pluginInstalled', function() {
+ var stub = sinon.stub(Gerrit, '_pluginInstalled');
+ Gerrit.install(function(p) { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ assert.isTrue(stub.calledOnce);
+ stub.restore();
+ });
+
+ test('install calls _pluginInstalled on error', function() {
+ var stub = sinon.stub(Gerrit, '_pluginInstalled');
+ Gerrit.install(function() {}, '0.0pre-alpha');
+ assert.isTrue(stub.calledOnce);
+ stub.restore();
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 21d76f1..ad8c135 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -64,6 +64,9 @@
var Gerrit = window.Gerrit || {};
+ // Number of plugins to initialize, -1 means 'not yet known'.
+ Gerrit._pluginsPending = -1;
+
Gerrit.getPluginName = function() {
console.warn('Gerrit.getPluginName is not supported in PolyGerrit.',
'Please use self.getPluginName() instead.');
@@ -85,12 +88,14 @@
if (opt_version && opt_version !== API_VERSION) {
console.warn('Only version ' + API_VERSION +
' is supported in PolyGerrit. ' + opt_version + ' was given.');
+ Gerrit._pluginInstalled();
return;
}
// TODO(andybons): Polyfill currentScript for IE10/11 (edge supports it).
var src = opt_src || (document.currentScript && document.currentScript.src);
callback(new Plugin(src));
+ Gerrit._pluginInstalled();
};
Gerrit.getLoggedIn = function() {
@@ -101,5 +106,20 @@
// NOOP since PolyGerrit doesn’t support GWT plugins.
};
+ Gerrit._setPluginsCount = function(count) {
+ Gerrit._pluginsPending = count;
+ if (Gerrit._arePluginsLoaded()) {
+ document.createElement('gr-reporting').pluginsLoaded();
+ }
+ };
+
+ Gerrit._pluginInstalled = function() {
+ Gerrit._setPluginsCount(Gerrit._pluginsPending - 1);
+ };
+
+ Gerrit._arePluginsLoaded = function() {
+ return Gerrit._pluginsPending === 0;
+ };
+
window.Gerrit = Gerrit;
})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index d823f8c..f34ffcf 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -14,7 +14,7 @@
limitations under the License.
-->
-<link rel="import" href="../../../behaviors/gr-path-list-behavior.html">
+<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<script src="../../../bower_components/es6-promise/dist/es6-promise.min.js"></script>
<script src="../../../bower_components/fetch/fetch.js"></script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 042a497..c5df0a2 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -22,8 +22,10 @@
<script src="../bower_components/web-component-tester/browser.js"></script>
<script>
var testFiles = [];
- var basePath = '../elements/';
+ var elementsPath = '../elements/';
+ var behaviorsPath = '../behaviors/';
+ // Elements tests.
[
'change/gr-account-entry/gr-account-entry_test.html',
'change/gr-account-list/gr-account-list_test.html',
@@ -94,10 +96,18 @@
'shared/gr-select/gr-select_test.html',
'shared/gr-storage/gr-storage_test.html',
].forEach(function(file) {
- file = basePath + file;
+ file = elementsPath + file;
testFiles.push(file);
testFiles.push(file + '?dom=shadow');
});
+ // Behaviors tests.
+ [
+ 'gr-path-list-behavior/gr-path-list-behavior_test.html',
+ ].forEach(function(file) {
+ file = behaviorsPath + file;
+ testFiles.push(file);
+ });
+
WCT.loadSuites(testFiles);
</script>
diff --git a/tools/bzl/asciidoc.bzl b/tools/bzl/asciidoc.bzl
new file mode 100644
index 0000000..17736cd
--- /dev/null
+++ b/tools/bzl/asciidoc.bzl
@@ -0,0 +1,338 @@
+def documentation_attributes():
+ return [
+ "toc",
+ 'newline="\\n"',
+ 'asterisk="*"',
+ 'plus="+"',
+ 'caret="^"',
+ 'startsb="["',
+ 'endsb="]"',
+ 'tilde="~"',
+ "last-update-label!",
+ "source-highlighter=prettify",
+ "stylesheet=DEFAULT",
+ "linkcss=true",
+ "prettifydir=.",
+ # Just a placeholder, will be filled in asciidoctor java binary:
+ "revnumber=%s",
+ ]
+
+
+def release_notes_attributes():
+ return [
+ 'toc',
+ 'newline="\\n"',
+ 'asterisk="*"',
+ 'plus="+"',
+ 'caret="^"',
+ 'startsb="["',
+ 'endsb="]"',
+ 'tilde="~"',
+ 'last-update-label!',
+ 'stylesheet=DEFAULT',
+ 'linkcss=true',
+ ]
+
+
+def _replace_macros_impl(ctx):
+ cmd = [
+ ctx.file._exe.path,
+ '--suffix', ctx.attr.suffix,
+ "-s", ctx.file.src.path,
+ "-o", ctx.outputs.out.path,
+ ]
+ if ctx.attr.searchbox:
+ cmd.append('--searchbox')
+ else:
+ cmd.append('--no-searchbox')
+ ctx.action(
+ inputs = [ctx.file._exe, ctx.file.src],
+ outputs = [ctx.outputs.out],
+ command = cmd,
+ progress_message = "Replacing macros in %s" % ctx.file.src.short_path,
+ )
+
+_replace_macros = rule(
+ implementation = _replace_macros_impl,
+ attrs = {
+ "_exe": attr.label(
+ default = Label("//Documentation:replace_macros.py"),
+ allow_single_file = True,
+ ),
+ "src": attr.label(
+ mandatory = True,
+ allow_single_file = [".txt"],
+ ),
+ "suffix": attr.string(mandatory = True),
+ "searchbox": attr.bool(default = True),
+ "out": attr.output(mandatory = True),
+ },
+)
+
+
+def _generate_asciidoc_args(ctx):
+ args = []
+ if ctx.attr.backend:
+ args.extend(["-b", ctx.attr.backend])
+ revnumber = False
+ for attribute in ctx.attr.attributes:
+ if attribute.startswith("revnumber="):
+ revnumber = True
+ else:
+ args.extend(["-a", attribute])
+ if revnumber:
+ args.extend([
+ "--revnumber-file", ctx.file.version.path,
+ ])
+ for src in ctx.files.srcs:
+ args.append(src.path)
+ return args
+
+
+def _invoke_replace_macros(name, src, suffix, searchbox):
+ fn = src
+ if fn.startswith(":"):
+ fn = src[1:]
+
+ _replace_macros(
+ name = "macros_%s_%s" % (name, fn),
+ src = src,
+ out = fn + suffix,
+ suffix = suffix,
+ searchbox = searchbox,
+ )
+
+ return ":" + fn + suffix, fn.replace(".txt", ".html")
+
+
+def _asciidoc_impl(ctx):
+ args = [
+ "--bazel",
+ "--in-ext", ".txt" + ctx.attr.suffix,
+ "--out-ext", ".html",
+ ]
+ args.extend(_generate_asciidoc_args(ctx))
+ ctx.action(
+ inputs = ctx.files.srcs + [ctx.executable._exe, ctx.file.version],
+ outputs = ctx.outputs.outs,
+ executable = ctx.executable._exe,
+ arguments = args,
+ progress_message = "Rendering asciidoctor files for %s" % ctx.label.name,
+ )
+
+_asciidoc = rule(
+ implementation = _asciidoc_impl,
+ attrs = {
+ "_exe": attr.label(
+ default = Label("//lib/asciidoctor:asciidoc"),
+ allow_files = True,
+ executable = True,
+ ),
+ "srcs": attr.label_list(mandatory = True, allow_files = True),
+ "version": attr.label(
+ default = Label("//:version.txt"),
+ allow_single_file = True,
+ ),
+ "suffix": attr.string(mandatory = True),
+ "backend": attr.string(),
+ "attributes": attr.string_list(),
+ "outs": attr.output_list(mandatory = True),
+ },
+)
+
+
+def _genasciidoc_htmlonly(
+ name,
+ srcs = [],
+ attributes = [],
+ backend = None,
+ searchbox = True,
+ **kwargs):
+ SUFFIX = "." + name + "_macros"
+ new_srcs = []
+ outs = ["asciidoctor.css"]
+
+ for src in srcs:
+ new_src, html_name = _invoke_replace_macros(name, src, SUFFIX, searchbox)
+ new_srcs.append(new_src)
+ outs.append(html_name)
+
+ _asciidoc(
+ name = name + "_gen",
+ srcs = new_srcs,
+ suffix = SUFFIX,
+ backend = backend,
+ attributes = attributes,
+ outs = outs,
+ )
+
+ native.filegroup(
+ name = name,
+ data = outs,
+ **kwargs
+ )
+
+
+def genasciidoc(
+ name,
+ srcs = [],
+ attributes = [],
+ backend = None,
+ searchbox = True,
+ resources = True,
+ **kwargs):
+ SUFFIX = "_htmlonly"
+
+ _genasciidoc_htmlonly(
+ name = name + SUFFIX if resources else name,
+ srcs = srcs,
+ attributes = attributes,
+ backend = backend,
+ searchbox = searchbox,
+ **kwargs
+ )
+
+ if resources:
+ htmlonly = ":" + name + SUFFIX
+ native.filegroup(
+ name = name,
+ srcs = [
+ htmlonly,
+ "//Documentation:resources",
+ ],
+ **kwargs
+ )
+
+
+def _asciidoc_html_zip_impl(ctx):
+ args = [
+ "--mktmp",
+ "-z", ctx.outputs.out.path,
+ "--in-ext", ".txt" + ctx.attr.suffix,
+ "--out-ext", ".html",
+ ]
+ args.extend(_generate_asciidoc_args(ctx))
+ ctx.action(
+ inputs = ctx.files.srcs + [ctx.executable._exe, ctx.file.version],
+ outputs = [ctx.outputs.out],
+ executable = ctx.executable._exe,
+ arguments = args,
+ progress_message = "Rendering asciidoctor files for %s" % ctx.label.name,
+ )
+
+_asciidoc_html_zip = rule(
+ implementation = _asciidoc_html_zip_impl,
+ attrs = {
+ "_exe": attr.label(
+ default = Label("//lib/asciidoctor:asciidoc"),
+ allow_files = True,
+ executable = True,
+ ),
+ "srcs": attr.label_list(mandatory = True, allow_files = True),
+ "version": attr.label(
+ default = Label("//:version.txt"),
+ allow_single_file = True,
+ ),
+ "suffix": attr.string(mandatory = True),
+ "backend": attr.string(),
+ "attributes": attr.string_list(),
+ },
+ outputs = {
+ "out": "%{name}.zip",
+ }
+)
+
+
+def _genasciidoc_htmlonly_zip(
+ name,
+ srcs = [],
+ attributes = [],
+ backend = None,
+ searchbox = True,
+ **kwargs):
+ SUFFIX = "." + name + "_expn"
+ new_srcs = []
+
+ for src in srcs:
+ new_src, _ = _invoke_replace_macros(name, src, SUFFIX, searchbox)
+ new_srcs.append(new_src)
+
+ _asciidoc_html_zip(
+ name = name,
+ srcs = new_srcs,
+ suffix = SUFFIX,
+ backend = backend,
+ attributes = attributes,
+ )
+
+
+def _asciidoc_zip_impl(ctx):
+ tmpdir = ctx.outputs.out.path + "_tmpdir"
+ cmd = [
+ "p=$PWD",
+ "mkdir -p %s" % tmpdir,
+ "unzip -q %s -d %s/%s/" % (ctx.file.src.path, tmpdir, ctx.attr.directory),
+ ]
+ for r in ctx.files.resources:
+ if r.path == r.short_path:
+ cmd.append("tar -cf- %s | tar -C %s -xf-" % (r.short_path, tmpdir))
+ else:
+ parent = r.path[:-len(r.short_path)]
+ cmd.append(
+ "tar -C %s -cf- %s | tar -C %s -xf-" % (parent, r.short_path, tmpdir))
+ cmd.extend([
+ "cd %s" % tmpdir,
+ "zip -qr $p/%s *" % ctx.outputs.out.path,
+ ])
+ ctx.action(
+ inputs = [ctx.file.src] + ctx.files.resources,
+ outputs = [ctx.outputs.out],
+ command = " && ".join(cmd),
+ progress_message =
+ "Generating asciidoctor zip file %s" % ctx.outputs.out.short_path,
+ )
+
+_asciidoc_zip = rule(
+ implementation = _asciidoc_zip_impl,
+ attrs = {
+ "src": attr.label(
+ mandatory = True,
+ allow_single_file = [".zip"],
+ ),
+ "resources": attr.label_list(mandatory = True, allow_files = True),
+ "directory": attr.string(mandatory = True),
+ },
+ outputs = {
+ "out": "%{name}.zip",
+ }
+)
+
+
+def genasciidoc_zip(
+ name,
+ srcs = [],
+ attributes = [],
+ directory = None,
+ backend = None,
+ searchbox = True,
+ resources = True,
+ **kwargs):
+ SUFFIX = "_htmlonly"
+
+ _genasciidoc_htmlonly_zip(
+ name = name + SUFFIX if resources else name,
+ srcs = srcs,
+ attributes = attributes,
+ backend = backend,
+ searchbox = searchbox,
+ **kwargs
+ )
+
+ if resources:
+ htmlonly = ":" + name + SUFFIX
+ _asciidoc_zip(
+ name = name,
+ src = htmlonly,
+ resources = ["//Documentation:resources"],
+ directory = directory,
+ )
diff --git a/tools/bzl/js.bzl b/tools/bzl/js.bzl
index 978059a..38d6c91 100644
--- a/tools/bzl/js.bzl
+++ b/tools/bzl/js.bzl
@@ -39,11 +39,12 @@
python = ctx.which("python")
script = ctx.path(ctx.attr._download_script)
- args = [python, script, "-o", dest, "-u", url]
+ sha1 = NPM_SHA1S[name]
+ args = [python, script, "-o", dest, "-u", url, "-v", sha1]
out = ctx.execute(args)
if out.return_code:
fail("failed %s: %s" % (args, out.stderr))
- ctx.file("BUILD", "filegroup(name='tarball', srcs=['%s'])" % base, False)
+ ctx.file("BUILD", "package(default_visibility=['//visibility:public'])\nfilegroup(name='tarball', srcs=['%s'])" % base, False)
npm_binary = repository_rule(
implementation=_npm_binary_impl,
@@ -55,12 +56,13 @@
})
-def _run_npm_binary_str(ctx, tarball, name):
+# for use in repo rules.
+def _run_npm_binary_str(ctx, tarball, args):
python_bin = ctx.which("python")
return " ".join([
python_bin,
ctx.path(ctx.attr._run_npm),
- ctx.path(tarball)])
+ ctx.path(tarball)] + args)
def _bower_archive(ctx):
@@ -72,7 +74,7 @@
cmd = [
ctx.which("python"),
ctx.path(ctx.attr._download_bower),
- '-b', '%s' % _run_npm_binary_str(ctx, ctx.attr._bower_archive, "bower"),
+ '-b', '%s' % _run_npm_binary_str(ctx, ctx.attr._bower_archive, []),
'-n', ctx.name,
'-p', ctx.attr.package,
'-v', ctx.attr.version,
@@ -235,6 +237,10 @@
for d in ctx.attr.deps:
versions += d.transitive_versions
+ licenses = set([])
+ for d in ctx.attr.deps:
+ licenses += d.transitive_versions
+
out_zip = ctx.outputs.zip
out_versions = ctx.outputs.version_json
@@ -259,6 +265,11 @@
mnemonic="BowerVersions",
command="(echo '{' ; for j in %s ; do cat $j; echo ',' ; done ; echo \\\"\\\":\\\"\\\"; echo '}') > %s" % (" ".join([v.path for v in versions]), out_versions.path))
+ return struct(
+ transitive_zipfiles=zips,
+ transitive_versions=versions,
+ transitive_licenses=licenses)
+
bower_component_bundle = rule(
_bower_component_bundle_impl,
@@ -268,3 +279,88 @@
"version_json": "%{name}-versions.json",
}
)
+
+def _vulcanize_impl(ctx):
+ destdir = ctx.outputs.vulcanized.path + ".dir"
+ zips = [z for d in ctx.attr.deps for z in d.transitive_zipfiles ]
+
+ hermetic_npm_binary = " ".join([
+ 'python',
+ "$p/" + ctx.file._run_npm.path,
+ "$p/" + ctx.file._vulcanize_archive.path,
+ '--inline-scripts',
+ '--inline-css',
+ '--strip-comments',
+ '--out-html', "$p/" + ctx.outputs.vulcanized.path,
+ ctx.file.app.path
+ ])
+
+ pkg_dir = ctx.attr.pkg.lstrip("/")
+ cmd = " && ".join([
+ # unpack dependencies.
+ "export PATH",
+ "p=$PWD",
+ "rm -rf %s" % destdir,
+ "mkdir -p %s/%s/bower_components" % (destdir, pkg_dir),
+ "for z in %s; do unzip -qd %s/%s/bower_components/ $z; done" % (
+ ' '.join([z.path for z in zips]), destdir, pkg_dir),
+ "tar -cf - %s | tar -C %s -xf -" % (" ".join([s.path for s in ctx.files.srcs]), destdir),
+ "cd %s" % destdir,
+ hermetic_npm_binary,
+ ])
+ ctx.action(
+ mnemonic = "Vulcanize",
+ inputs = [ctx.file._run_npm, ctx.file.app,
+ ctx.file._vulcanize_archive
+ ] + list(zips) + ctx.files.srcs,
+ outputs = [ctx.outputs.vulcanized],
+ command = cmd)
+
+ hermetic_npm_command = "export PATH && " + " ".join([
+ 'python',
+ ctx.file._run_npm.path,
+ ctx.file._crisper_archive.path,
+ "--always-write-script",
+ "--source", ctx.outputs.vulcanized.path,
+ "--html", ctx.outputs.html.path,
+ "--js", ctx.outputs.js.path])
+
+ ctx.action(
+ mnemonic = "Crisper",
+ inputs = [ctx.file._run_npm, ctx.file.app,
+ ctx.file._crisper_archive, ctx.outputs.vulcanized],
+ outputs = [ctx.outputs.js, ctx.outputs.html],
+ command = hermetic_npm_command)
+
+
+_vulcanize_rule = rule(
+ _vulcanize_impl,
+ attrs = {
+ "deps": attr.label_list(providers=["transitive_zipfiles"]),
+ "app": attr.label(mandatory=True, allow_single_file=True),
+ "srcs": attr.label_list(allow_files=[".js", ".html", ".txt", ".css", ".ico"]),
+
+ "pkg": attr.string(mandatory=True),
+ "_run_npm": attr.label(
+ default=Label("//tools/js:run_npm_binary.py"),
+ allow_single_file=True
+ ),
+ "_vulcanize_archive": attr.label(
+ default=Label("@vulcanize//:%s" % _npm_tarball("vulcanize")),
+ allow_single_file=True
+ ),
+ "_crisper_archive": attr.label(
+ default=Label("@crisper//:%s" % _npm_tarball("crisper")),
+ allow_single_file=True
+ ),
+ },
+ outputs = {
+ "vulcanized": "%{name}.vulcanized.html",
+ "html": "%{name}.crisped.html",
+ "js": "%{name}.crisped.js",
+ }
+)
+
+def vulcanize(*args, **kwargs):
+ """Vulcanize runs vulcanize and crisper on a set of sources."""
+ _vulcanize_rule(*args, pkg=PACKAGE_NAME, **kwargs)
diff --git a/tools/bzl/license.bzl b/tools/bzl/license.bzl
index 37cc70c..60ee60b 100644
--- a/tools/bzl/license.bzl
+++ b/tools/bzl/license.bzl
@@ -2,7 +2,7 @@
def normalize_target_name(target):
return target.replace("//", "").replace("/", "__").replace(":", "___")
-def license_map(name, targets = [], opts = []):
+def license_map(name, targets = [], opts = [], **kwargs):
"""Generate XML for all targets that depend directly on a LICENSE file"""
xmls = []
tools = [ "//tools/bzl:license-map.py", "//lib:all-licenses" ]
@@ -28,7 +28,8 @@
name = "gen_license_txt_" + name,
cmd = "python $(location //tools/bzl:license-map.py) %s %s > $@" % (" ".join(opts), " ".join(xmls)),
outs = [ name + ".txt" ],
- tools = tools
+ tools = tools,
+ **kwargs
)
def license_test(name, target):
diff --git a/tools/bzl/pkg_war.bzl b/tools/bzl/pkg_war.bzl
index ef0fe51..aa7d07f 100644
--- a/tools/bzl/pkg_war.bzl
+++ b/tools/bzl/pkg_war.bzl
@@ -124,7 +124,7 @@
outputs = {'war' : '%{name}.war'},
)
-def pkg_war(name, ui = 'ui_optdbg', context = []):
+def pkg_war(name, ui = 'ui_optdbg', context = [], **kwargs):
ui_deps = []
if ui:
ui_deps.append('//gerrit-gwtui:%s' % ui)
@@ -136,4 +136,5 @@
'//gerrit-main:main_bin_deploy.jar',
'//gerrit-war:webapp_assets',
],
+ **kwargs
)
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index bdd1794..32f57e86 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -4,7 +4,8 @@
deps = [],
srcs = [],
resources = [],
- manifest_entries = []):
+ manifest_entries = [],
+ **kwargs):
# TODO(davido): Fix stamping: run git describe in plugin directory
# https://github.com/bazelbuild/bazel/issues/1758
manifest_lines = [
@@ -31,4 +32,5 @@
':%s__plugin' % name,
],
visibility = ['//visibility:public'],
+ **kwargs
)
diff --git a/tools/eclipse/BUCK b/tools/eclipse/BUCK
index dfd271d..a8b3f01 100644
--- a/tools/eclipse/BUCK
+++ b/tools/eclipse/BUCK
@@ -15,7 +15,6 @@
'//gerrit-reviewdb:client_tests',
'//gerrit-server:server',
'//gerrit-server:server_tests',
- '//lib:jimfs',
'//lib/asciidoctor:asciidoc_lib',
'//lib/asciidoctor:doc_indexer_lib',
'//lib/auto:auto-value',
diff --git a/tools/js/BUILD b/tools/js/BUILD
index e69de29..fedaf7f 100644
--- a/tools/js/BUILD
+++ b/tools/js/BUILD
@@ -0,0 +1 @@
+exports_files(["run_npm_binary.py"])