Merge branch 'stable-2.14'
* stable-2.14:
Leave assignee feature always enabled
Hide assignee by default in Gerrit changes table
Change-Id: I7bc3b19ea72affe8417928e2ffed08de8d6e6dfb
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index f64f739..20d4e45 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -850,6 +850,15 @@
Note that this permission is named `submitAs` in the `project.config`
file.
+[[category_view_private_changes]]
+=== View Private Changes
+
+This category permits users to view all private changes.
+
+The change owner and any explicitly added reviewers can always see
+private changes (even without having the `View Private Changes` access
+right assigned).
+
[[category_view_drafts]]
=== View Drafts
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 72b5595..bb637f8 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -397,8 +397,14 @@
the "Switch Account" link is displayed next to "Sign Out".
+
When `auth.type` does not normally enable this URL administrators may
-set this to `login/` or `$canonicalWebUrl/login`, allowing users to
-begin a new web session.
+set this to `login/`, allowing users to begin a new web session. This value
+is used as an href in PolyGerrit and the GWT UI, so absolute URLs like
+`https://someotherhost/login` work as well.
++
+If a ${path} parameter is included, then PolyGerrit will substitute the
+currently viewed path in the link. Be aware that this path will include
+a leading slash, so a value like this might be appropriate: `/login${path}`.
+Note: in the GWT UI this substitution for ${path} is *always* `/`.
[[auth.cookiePath]]auth.cookiePath::
+
@@ -1104,6 +1110,16 @@
Default is "Reply and score". In the user interface it becomes "Reply
and score (Shortcut: a)".
+[[change.robotCommentSizeLimit]]change.robotCommentSizeLimit::
++
+Maximum allowed size of a robot comment that will be accepted. Robot comments
+which exceed the indicated size will be rejected on addition. The specified
+value is interpreted as the maximum size in bytes of the JSON representation of
+the robot comment. Common unit suffixes of 'k', 'm', or 'g' are supported.
+Zero or negative values allow robot comments of unlimited size.
++
+The default limit is 1024kB.
+
[[changeCleanup]]
=== Section changeCleanup
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 34f39c8..90b0a83 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -291,6 +291,20 @@
check. If the `branchOrder` section is not defined then the mergeability of a
change into other branches will not be done.
+[[reviewer-section]]
+=== reviewer section
+
+Defines config options to adjust a project's reviewer workflow such as enabling
+reviewers and CCs by email.
+
+[[reviewer.enableByEmail]]reviewer.enableByEmail::
++
+A boolean indicating if reviewers and CCs that do not currently have a Gerrit
+account can be added to a change by providing their email address.
+
+Default is `INHERIT`, which means that this property is inherited from
+the parent project. If the property is not set in any parent project, the
+default value is `FALSE`.
[[file-groups]]
== The file +groups+
diff --git a/Documentation/dev-plugins-pg.txt b/Documentation/dev-plugins-pg.txt
new file mode 100644
index 0000000..9411570
--- /dev/null
+++ b/Documentation/dev-plugins-pg.txt
@@ -0,0 +1,24 @@
+= Gerrit Code Review - PolyGerrit Plugin Development
+
+CAUTION: Work in progress. Hard hat area. +
+This document will be populated with details along with implementation. +
+link:https://groups.google.com/d/topic/repo-discuss/vb8WJ4m0hK0/discussion[Join the discussion.]
+
+== Plugin loading and initialization
+
+link:https://gerrit-review.googlesource.com/Documentation/js-api.html#_entry_point[Entry point] for the plugin and the loading method is based on link:http://w3c.github.io/webcomponents/spec/imports/[HTML Imports] spec.
+
+* Plugin provides index.html, similar to link:https://gerrit-review.googlesource.com/Documentation/dev-plugins.html#deployment[.js Web UI plugins]
+* index.html contains a `dom-module` tag with a script that uses `Gerrit.install()`.
+* PolyGerrit imports index.html along with all required resources defined in it (fonts, styles, etc)
+* For standalone plugins, the entry point file is a `pluginname.html` file located in `gerrit-site/plugins` folder, where pluginname is an alphanumeric plugin name.
+
+Here's a sample `myplugin.html`:
+
+``` html
+<dom-module id="my-plugin">
+ <script>
+ Gerrit.install(function() { console.log('Ready.'); });
+ </script>
+</dom-module>
+```
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 75ef9e6..b6d2c53 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -704,6 +704,37 @@
}
====
+[[command_options]]
+=== Command Options ===
+
+Plugins can provide additional options for each of the gerrit ssh and the
+REST API commands by implementing the DynamicBean interface and registering
+it to a command class name in the plugin module's `configure()` method. The
+plugin's name will be prepended to the name of each @Option annotation found
+on the DynamicBean object provided by the plugin. The example below shows a
+plugin that adds an option to log a value from the gerrit 'ban-commits'
+ssh command.
+
+[source, java]
+----
+public class SshModule extends AbstractModule {
+ private static final Logger log = LoggerFactory.getLogger(SshModule.class);
+
+ @Override
+ protected void configure() {
+ bind(DynamicOptions.DynamicBean.class)
+ .annotatedWith(Exports.named(
+ com.google.gerrit.sshd.commands.BanCommitCommand.class))
+ .to(BanOptions.class);
+ }
+
+ public static class BanOptions implements DynamicOptions.DynamicBean {
+ @Option(name = "--log", aliases = { "-l" }, usage = "Say Hello in the Log")
+ private void parse(String arg) {
+ log.error("Say Hello in the Log " + arg);
+ }
+ }
+----
[[simple-configuration]]
== Simple Configuration in `gerrit.config`
@@ -1209,6 +1240,7 @@
@Override
public void onPluginLoad() {
Plugin.get().panel(GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
+ "my_panel_name",
new Panel.EntryPoint() {
@Override
public void onLoad(Panel panel) {
@@ -1220,6 +1252,23 @@
}
----
+Change Screen panel ordering may be specified in the
+project config. Values may be either "plugin name" or
+"plugin name"."panel name".
+Panels not specified in the config will be added
+to the end in load order. Panels specified in the config that
+are not found will be ignored.
+
+Example config:
+----
+[extension-panels "CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK"]
+ panel = helloworld.change_id
+ panel = myotherplugin
+ panel = myplugin.my_panel_name
+----
+
+
+
[[actions]]
=== Actions
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 61b9c30..9d8ea34 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -482,9 +482,50 @@
$ git push origin HEAD:refs/heads/master -o topic=multi-master
----
+[[private-changes]]
+== Private Changes
+
+Private changes are changes that are only visible to their owners and
+reviewers. Private changes are useful in a number of cases:
+
+* You want to check what the change looks before formal review starts.
+ By marking the change private without reviewers, nobody can't
+ prematurely comment on your changes.
+
+* You want to use Gerrit to sync data between different devices. By
+ creating a private throwaway change without reviewers, you can push
+ from one device, and fetch to another device.
+
+* You want to do code review on a change that has sensitive
+ aspects. By reviewing a security fix in a private change,
+ outsiders can't discover the fix before it is pushed out. Even after
+ merging the change, the review can be kept private.
+
+To create a private change, you push it with the `private` option.
+
+.Push a private change
+----
+ $ git commit
+ $ git push origin HEAD:refs/for/master%private
+----
+
+The change will remain private on subsequent pushes until you specify
+the `remove-private` option. Alternatively, the web UI provides buttons
+to mark a change private and non-private again.
+
+For CI systems that must verify private changes, a special permission
+can be granted
+(link:access-control.html#category_view_private_changes[View Private Changes]).
+In that case, care should be taken to prevent the CI system from
+exposing secret details.
+
+
[[drafts]]
== Working with Drafts
+Drafts is a deprecated feature and will be removed soon. Consider using
+private changes instead.
+
Changes can be uploaded as drafts. By default draft changes are only
visible to the change owner. This gives you the possibility to have
some staging before making your changes visible to the reviewers. Draft
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 091dbb3..e90e3e5 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -90,6 +90,9 @@
* `notedb/auto_rebuild_latency`: NoteDb auto-rebuilding latency by table.
* `notedb/auto_rebuild_failure_count`: NoteDb auto-rebuilding attempts that
failed by table.
+* `notedb/external_id_update_count`: Total number of external ID updates.
+* `notedb/read_all_external_ids_latency`: Latency for reading all
+external ID's from NoteDb.
=== Reviewer Suggestion
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 69afaa5..5d32eba 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2107,6 +2107,47 @@
}
----
+[[mark-private]]
+=== Mark Private
+--
+'PUT /changes/link:#change-id[\{change-id\}]/private'
+--
+
+Marks the change to be private. Note users can only mark own changes as private.
+
+.Request
+----
+ Set /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 201 Created
+----
+
+If the change was already private the response is "`200 OK`".
+
+[[unmark-private]]
+=== Unmark Private
+--
+'DELETE /changes/link:#change-id[\{change-id\}]/private'
+--
+
+Marks the change to be non-private. Note users can only unmark own private
+changes.
+
+.Request
+----
+ DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
+If the change was already not private, the response is "`409 Conflict`".
+
[[edit-endpoints]]
== Change Edit Endpoints
@@ -2764,6 +2805,41 @@
}
----
+If link:config-project-config.html#reviewer.enableByEmail[reviewer.enableByEmail] is set
+for the project, reviewers and CCs are not required to have a Gerrit account. If you POST
+an email address of a reviewer or CC then, they will be added to the change even if they
+don't have a Gerrit account.
+
+If this option is disabled, the request would fail with `400 Bad Request` if the email
+address can't be resolved to an active Gerrit account.
+
+Note that the name is optional so both "un.registered@reviewer.com" and
+"John Doe <un.registered@reviewer.com>" are valid inputs.
+
+Reviewers without Gerrit accounts can only be added on changes visible to anonymous users.
+
+.Request
+----
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "reviewer": "John Doe <un.registered@reviewer.com>"
+ }
+----
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "input": "John Doe <un.registered@reviewer.com>"
+ }
+----
+
[[delete-reviewer]]
=== Delete Reviewer
--
@@ -4256,6 +4332,72 @@
}
----
+[[apply-fix]]
+=== Apply Fix
+--
+'POST /changes/<<change-id,\{change-id\}>>/revisions/<<revision-id,\{revision-id\}>>/fixes/<<fix-id,\{fix-id\}>>/apply'
+--
+
+Applies a suggested fix by creating a change edit which includes the
+modifications indicated by the fix suggestion. If a change edit already exists,
+it will be updated accordingly. A fix can only be applied if no change edit
+exists and the fix refers to the current patch set, or the fix refers to the
+patch set on which the change edit is based.
+
+.Request
+----
+ POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/fixes/8f605a55_f6aa4ecc/apply HTTP/1.0
+----
+
+If the fix was successfully applied, an <<edit-info,EditInfo>> describing the
+resulting change edit is returned.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "commit":{
+ "parents":[
+ {
+ "commit":"1eee2c9d8f352483781e772f35dc586a69ff5646",
+ }
+ ],
+ "author":{
+ "name":"John Doe",
+ "email":"john.doe@example.com",
+ "date":"2013-05-07 15:21:27.000000000",
+ "tz":120
+ },
+ "committer":{
+ "name":"Jane Doe",
+ "email":"jane.doe@example.com",
+ "date":"2013-05-07 15:35:43.000000000",
+ "tz":120
+ },
+ "subject":"Implement feature X",
+ "message":"Implement feature X\n\nWith this feature ..."
+ },
+ "base_revision":"674ac754f91e64a0efb8087e59a176484bd534d1"
+ }
+----
+
+If the application failed e.g. due to conflicts with an existing change edit,
+the response "`409 Conflict`" including an error message in the response body
+is returned.
+
+.Response
+----
+ HTTP/1.1 409 Conflict
+ Content-Disposition: attachment
+ Content-Type: text/plain; charset=UTF-8
+
+ The existing change edit could not be merged with another tree.
+----
+
[[list-files]]
=== List Files
--
@@ -4710,7 +4852,8 @@
Cherry picks a revision to a destination branch.
The commit message and destination branch must be provided in the request body inside a
-link:#cherrypick-input[CherryPickInput] entity.
+link:#cherrypick-input[CherryPickInput] entity. If the commit message
+does not specify a Change-Id, a new one is picked for the destination change.
.Request
----
@@ -5259,6 +5402,10 @@
Author of the message as an
link:rest-api-accounts.html#account-info[AccountInfo] entity. +
Unset if written by the Gerrit system.
+|`real_author` |optional|
+Real author of the message as an
+link:rest-api-accounts.html#account-info[AccountInfo] entity. +
+Set if the message was posted on behalf of another user.
|`date` ||
The link:rest-api.html#timestamp[timestamp] this message was posted.
|`message` ||The text left by the user.
@@ -5592,7 +5739,7 @@
|`commit` ||The commit of change edit as
link:#commit-info[CommitInfo] entity.
|`base_revision`||The revision of the patch set the change edit is based on.
-|`fetch` ||
+|`fetch` |optional|
Information about how to fetch this patch set. The fetch information is
provided as a map that maps the protocol name ("`git`", "`http`",
"`ssh`") to link:#fetch-info[FetchInfo] entities.
@@ -5671,8 +5818,8 @@
for input objects.
|`description` ||A description of the suggested fix.
|`replacements` ||A list of <<fix-replacement-info,FixReplacementInfo>>
-entities indicating how the content of the file on which the comment was placed
-should be modified. They should refer to non-overlapping regions.
+entities indicating how the content of one or several files should be modified.
+Within a file, they should refer to non-overlapping regions.
|==========================
[[fix-replacement-info]]
@@ -5683,10 +5830,13 @@
[options="header",cols="1,6"]
|==========================
|Field Name |Description
-|`path` |The path of the file which should be modified. Modifications
-are only allowed for the file on which the corresponding comment was placed.
+|`path` |The path of the file which should be modified. Any file in
+the repository may be modified.
|`range` |A <<comment-range,CommentRange>> indicating which content
-of the file should be replaced.
+of the file should be replaced. Lines in the file are assumed to be separated
+by the line feed character, the carriage return character, the carriage return
+followed by the line feed character, or one of the other Unicode linebreak
+sequences supported by Java.
|`replacement` |The content which should be used instead of the current one.
|==========================
@@ -6138,6 +6288,10 @@
|`approvals` |
The approvals of the reviewer as a map that maps the label names to the
approval values ("`-2`", "`-1`", "`0`", "`+1`", "`+2`").
+|`_account_id` |
+This field is inherited from `AccountInfo` but is optional here if an
+unregistered reviewer was added by email. See
+link:rest-api-changes.html#add-reviewer[add-reviewer] for details.
|==========================
[[reviewer-input]]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 72c6a39..17b0192 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2060,6 +2060,60 @@
Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY...
----
+
+[[cherry-pick-commit]]
+=== Cherry Pick Commit
+--
+'POST /projects/link:#project-name[\{project-name\}]/commits/link:#commit-id[\{commit-id\}]/cherrypick'
+--
+
+Cherry-picks a commit of a project to a destination branch.
+
+The destination branch must be provided in the request body inside a
+link:rest-api-changes.html#cherrypick-input[CherryPickInput] entity.
+If the commit message is not set, the commit message of the source
+commit will be used.
+
+.Request
+----
+ POST /projects/work%2Fmy-project/commits/a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96/cherrypick HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "message" : "Implementing Feature X",
+ "destination" : "release-branch"
+ }
+----
+
+As response a link:rest-api-changes.html#change-info[ChangeInfo] entity is returned that
+describes the resulting cherry-picked change.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
+ "project": "myProject",
+ "branch": "release-branch",
+ "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
+ "subject": "Implementing Feature X",
+ "status": "NEW",
+ "created": "2013-02-01 09:59:32.126000000",
+ "updated": "2013-02-21 11:16:36.775000000",
+ "mergeable": true,
+ "insertions": 12,
+ "deletions": 11,
+ "_number": 3965,
+ "owner": {
+ "name": "John Doe"
+ }
+ }
+----
+
[[dashboard-endpoints]]
== Dashboard Endpoints
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 392d5cf..90e1979 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -367,6 +367,13 @@
Mergeability of abandoned changes is not computed. This operator will
not find any abandoned but mergeable changes.
+
+[[private]]
+is:private::
++
+True if the change is private, ie. only visible to owner and its
+reviewers.
+
[[status]]
status:open, status:pending::
+
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 9efbb21..8f716d1 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -212,6 +212,24 @@
git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental -o topic=driver/i42
----
+[[private]]
+==== Private Changes
+
+To push a private change or to turn a change private on push the `private`
+option can be specified:
+
+----
+ git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%private
+----
+
+Omitting the `private` option when pushing updates to a private change
+doesn't make change non-private again. To remove the private
+flag from a change on push, explicitly specify the `remove-private` option:
+
+----
+ git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%remove-private
+----
+
[[message]]
==== Message
diff --git a/README.md b/README.md
index 78c8477..1ca01d5 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,7 @@
## Source
Our canonical Git repository is located on [googlesource.com](https://gerrit.googlesource.com/gerrit).
-There is a mirror of the repository on [Github](https://github.com/gerrit-review/gerrit).
+There is a mirror of the repository on [Github](https://github.com/GerritCodeReview/gerrit).
## Reporting bugs
diff --git a/WORKSPACE b/WORKSPACE
index 573aab2..64a06ae 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -169,8 +169,8 @@
maven_jar(
name = "joda_time",
- artifact = "joda-time:joda-time:2.9.4",
- sha1 = "1c295b462f16702ebe720bbb08f62e1ba80da41b",
+ artifact = "joda-time:joda-time:2.9.8",
+ sha1 = "03986e1763e5df02ad7fc040ecb555193a8436bb",
)
maven_jar(
@@ -201,8 +201,8 @@
maven_jar(
name = "juniversalchardet",
- artifact = "com.googlecode.juniversalchardet:juniversalchardet:1.0.3",
- sha1 = "cd49678784c46aa8789c060538e0154013bb421b",
+ artifact = "com.github.albfernandez:juniversalchardet:2.0.0",
+ sha1 = "28c59f58f5adcc307604602e2aa89e2aca14c554",
)
SLF4J_VERS = "1.7.7"
@@ -257,8 +257,8 @@
maven_jar(
name = "commons_codec",
- artifact = "commons-codec:commons-codec:1.4",
- sha1 = "4216af16d38465bbab0f3dff8efa14204f7a399a",
+ artifact = "commons-codec:commons-codec:1.10",
+ sha1 = "4b95f4897fa13f2cd904aee711aeafc0c5295cd8",
)
maven_jar(
@@ -269,8 +269,8 @@
maven_jar(
name = "commons_compress",
- artifact = "org.apache.commons:commons-compress:1.12",
- sha1 = "84caa68576e345eb5e7ae61a0e5a9229eb100d7b",
+ artifact = "org.apache.commons:commons-compress:1.13",
+ sha1 = "15c5e9584200122924e50203ae210b57616b75ee",
)
maven_jar(
@@ -323,8 +323,8 @@
maven_jar(
name = "pegdown",
- artifact = "org.pegdown:pegdown:1.4.2",
- sha1 = "d96db502ed832df867ff5d918f05b51ba3879ea7",
+ artifact = "org.pegdown:pegdown:1.6.0",
+ sha1 = "231ae49d913467deb2027d0b8a0b68b231deef4f",
)
maven_jar(
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml
index d9d701c..d7c39ff 100644
--- a/gerrit-acceptance-framework/pom.xml
+++ b/gerrit-acceptance-framework/pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-acceptance-framework</artifactId>
- <version>2.14-SNAPSHOT</version>
+ <version>2.15-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Acceptance Test Framework</name>
<description>Framework for Gerrit's acceptance tests</description>
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index c4be97c..0c93b52 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -88,6 +88,7 @@
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.mail.send.EmailHeader;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -1201,21 +1202,29 @@
}
protected void assertNotifyTo(TestAccount expected) {
+ assertNotifyTo(expected.emailAddress);
+ }
+
+ protected void assertNotifyTo(Address expected) {
assertThat(sender.getMessages()).hasSize(1);
Message m = sender.getMessages().get(0);
- assertThat(m.rcpt()).containsExactly(expected.emailAddress);
+ assertThat(m.rcpt()).containsExactly(expected);
assertThat(((EmailHeader.AddressList) m.headers().get("To")).getAddressList())
- .containsExactly(expected.emailAddress);
+ .containsExactly(expected);
assertThat(m.headers().get("CC").isEmpty()).isTrue();
}
protected void assertNotifyCc(TestAccount expected) {
+ assertNotifyCc(expected.emailAddress);
+ }
+
+ protected void assertNotifyCc(Address expected) {
assertThat(sender.getMessages()).hasSize(1);
Message m = sender.getMessages().get(0);
- assertThat(m.rcpt()).containsExactly(expected.emailAddress);
+ assertThat(m.rcpt()).containsExactly(expected);
assertThat(m.headers().get("To").isEmpty()).isTrue();
assertThat(((EmailHeader.AddressList) m.headers().get("CC")).getAddressList())
- .containsExactly(expected.emailAddress);
+ .containsExactly(expected);
}
protected void assertNotifyBcc(TestAccount expected) {
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 e136bb3..cef6128 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
@@ -18,6 +18,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.US_ASCII;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -25,10 +26,11 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountByEmailCache;
import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.ExternalId;
-import com.google.gerrit.server.account.ExternalIdsUpdate;
+import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.index.account.AccountIndexer;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gerrit.testutil.SshMode;
@@ -51,6 +53,7 @@
private final Map<String, TestAccount> accounts;
private final SchemaFactory<ReviewDb> reviewDbProvider;
+ private final AccountsUpdate accountsUpdate;
private final VersionedAuthorizedKeys.Accessor authorizedKeys;
private final GroupCache groupCache;
private final SshKeyCache sshKeyCache;
@@ -62,6 +65,7 @@
@Inject
AccountCreator(
SchemaFactory<ReviewDb> schema,
+ AccountsUpdate accountsUpdate,
VersionedAuthorizedKeys.Accessor authorizedKeys,
GroupCache groupCache,
SshKeyCache sshKeyCache,
@@ -71,6 +75,7 @@
ExternalIdsUpdate.Server externalIdsUpdate) {
accounts = new HashMap<>();
reviewDbProvider = schema;
+ this.accountsUpdate = accountsUpdate;
this.authorizedKeys = authorizedKeys;
this.groupCache = groupCache;
this.sshKeyCache = sshKeyCache;
@@ -81,7 +86,12 @@
}
public synchronized TestAccount create(
- String username, String email, String fullName, String... groups) throws Exception {
+ @Nullable String username,
+ @Nullable String email,
+ @Nullable String fullName,
+ String... groups)
+ throws Exception {
+
TestAccount account = accounts.get(username);
if (account != null) {
return account;
@@ -90,8 +100,11 @@
Account.Id id = new Account.Id(db.nextAccountId());
List<ExternalId> extIds = new ArrayList<>(2);
- String httpPass = "http-pass";
- extIds.add(ExternalId.createUsername(username, id, httpPass));
+ String httpPass = null;
+ if (username != null) {
+ httpPass = "http-pass";
+ extIds.add(ExternalId.createUsername(username, id, httpPass));
+ }
if (email != null) {
extIds.add(ExternalId.createEmail(id, email));
@@ -101,7 +114,7 @@
Account a = new Account(id, TimeUtil.nowTs());
a.setFullName(fullName);
a.setPreferredEmail(email);
- db.accounts().insert(Collections.singleton(a));
+ accountsUpdate.insert(db, a);
if (groups != null) {
for (String n : groups) {
@@ -114,28 +127,36 @@
}
KeyPair sshKey = null;
- if (SshMode.useSsh()) {
+ if (SshMode.useSsh() && username != null) {
sshKey = genSshKey();
authorizedKeys.addKey(id, publicKey(sshKey, email));
sshKeyCache.evict(username);
}
- accountCache.evictByUsername(username);
+ if (username != null) {
+ accountCache.evictByUsername(username);
+ }
byEmailCache.evict(email);
indexer.index(id);
account = new TestAccount(id, username, email, fullName, sshKey, httpPass);
- accounts.put(username, account);
+ if (username != null) {
+ accounts.put(username, account);
+ }
return account;
}
}
- public TestAccount create(String username, String group) throws Exception {
+ public TestAccount create(@Nullable String username, String group) throws Exception {
return create(username, null, username, group);
}
- public TestAccount create(String username) throws Exception {
+ public TestAccount create() throws Exception {
+ return create(null);
+ }
+
+ public TestAccount create(@Nullable String username) throws Exception {
return create(username, null, username, (String[]) null);
}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 58cdf96..b7b6f6a 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -45,7 +45,6 @@
import java.net.URI;
import java.nio.file.Paths;
import java.util.concurrent.BrokenBarrierException;
-import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -133,17 +132,14 @@
static GerritServer start(Description desc, Config baseConfig) throws Exception {
Config cfg = desc.buildConfig(baseConfig);
Logger.getLogger("com.google.gerrit").setLevel(Level.DEBUG);
- final CyclicBarrier serverStarted = new CyclicBarrier(2);
- final Daemon daemon =
+ CyclicBarrier serverStarted = new CyclicBarrier(2);
+ Daemon daemon =
new Daemon(
- new Runnable() {
- @Override
- public void run() {
- try {
- serverStarted.await();
- } catch (InterruptedException | BrokenBarrierException e) {
- throw new RuntimeException(e);
- }
+ () -> {
+ try {
+ serverStarted.await();
+ } catch (InterruptedException | BrokenBarrierException e) {
+ throw new RuntimeException(e);
}
},
Paths.get(baseConfig.getString("gerrit", null, "tempSiteDir")));
@@ -173,24 +169,17 @@
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError =
daemonService.submit(
- new Callable<Void>() {
- @Override
- public Void call() throws Exception {
- int rc =
- daemon.main(
- new String[] {
- "-d",
- site.getPath(),
- "--headless",
- "--console-log",
- "--show-stack-trace",
- });
- if (rc != 0) {
- System.err.println("Failed to start Gerrit daemon");
- serverStarted.reset();
- }
- return null;
+ () -> {
+ int rc =
+ daemon.main(
+ new String[] {
+ "-d", site.getPath(), "--headless", "--console-log", "--show-stack-trace",
+ });
+ if (rc != 0) {
+ System.err.println("Failed to start Gerrit daemon");
+ serverStarted.reset();
}
+ return null;
});
serverStarted.await();
System.out.println("Gerrit Server Started");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index e34223e8..abaaefc 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -26,6 +26,7 @@
import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL;
import static com.google.gerrit.server.StarredChangesUtil.IGNORE_LABEL;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -59,15 +60,15 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.gpg.Fingerprint;
import com.google.gerrit.gpg.PublicKeyStore;
-import com.google.gerrit.gpg.server.GpgKeys;
import com.google.gerrit.gpg.testutil.TestKey;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.AccountByEmailCache;
-import com.google.gerrit.server.account.ExternalId;
-import com.google.gerrit.server.account.ExternalIdsUpdate;
import com.google.gerrit.server.account.WatchConfig;
import com.google.gerrit.server.account.WatchConfig.NotifyType;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.RefPattern;
@@ -116,6 +117,8 @@
@Inject private AccountByEmailCache byEmailCache;
+ @Inject private ExternalIds externalIds;
+
@Inject private ExternalIdsUpdate.User externalIdsUpdateFactory;
private ExternalIdsUpdate externalIdsUpdate;
@@ -913,7 +916,11 @@
Iterable<String> expectedFps =
expected.transform(k -> BaseEncoding.base16().encode(k.getPublicKey().getFingerprint()));
Iterable<String> actualFps =
- GpgKeys.getGpgExtIds(db, currAccountId).transform(e -> e.key().id());
+ externalIds
+ .byAccount(db, currAccountId, SCHEME_GPGKEY)
+ .stream()
+ .map(e -> e.key().id())
+ .collect(toSet());
assertThat(actualFps).named("external IDs in database").containsExactlyElementsIn(expectedFps);
// Check raw stored keys.
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 c92006b..690c3dd2 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
@@ -181,6 +181,70 @@
}
@Test
+ public void setPrivateByOwner() throws Exception {
+ TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+ PushOneCommit.Result result =
+ pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
+
+ setApiUser(user);
+ String changeId = result.getChangeId();
+ assertThat(gApi.changes().id(changeId).get().isPrivate).isFalse();
+ gApi.changes().id(changeId).setPrivate(true);
+ assertThat(gApi.changes().id(changeId).get().isPrivate).isTrue();
+ gApi.changes().id(changeId).setPrivate(false);
+ assertThat(gApi.changes().id(changeId).get().isPrivate).isFalse();
+ }
+
+ @Test
+ public void setPrivateByOtherUser() throws Exception {
+ TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+ PushOneCommit.Result result =
+ pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
+
+ assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isFalse();
+ exception.expect(AuthException.class);
+ exception.expectMessage("not allowed to mark private");
+ gApi.changes().id(result.getChangeId()).setPrivate(true);
+ }
+
+ @Test
+ public void accessPrivate() throws Exception {
+ TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
+ PushOneCommit.Result result =
+ pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
+
+ setApiUser(user);
+ gApi.changes().id(result.getChangeId()).setPrivate(true);
+ // Owner can always access its private changes.
+ assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
+
+ // Add admin as a reviewer.
+ gApi.changes().id(result.getChangeId()).addReviewer(admin.getId().toString());
+
+ // This change should be visible for admin as a reviewer.
+ setApiUser(admin);
+ assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
+
+ // Remove admin from reviewers.
+ gApi.changes().id(result.getChangeId()).reviewer(admin.getId().toString()).remove();
+
+ // This change should not be visible for admin anymore.
+ exception.expect(ResourceNotFoundException.class);
+ exception.expectMessage("Not found: " + result.getChangeId());
+ gApi.changes().id(result.getChangeId());
+ }
+
+ @Test
+ public void privateChangeOfOtherUserCanBeAccessedWithPermission() throws Exception {
+ PushOneCommit.Result result = createChange();
+ gApi.changes().id(result.getChangeId()).setPrivate(true);
+
+ allow(Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS, "refs/*");
+ setApiUser(user);
+ assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
+ }
+
+ @Test
public void getAmbiguous() throws Exception {
PushOneCommit.Result r1 = createChange();
String changeId = r1.getChangeId();
@@ -280,6 +344,30 @@
}
@Test
+ public void abandonNotAllowedWithoutPermission() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
+ setApiUser(user);
+ exception.expect(AuthException.class);
+ exception.expectMessage("abandon not permitted");
+ gApi.changes().id(changeId).abandon();
+ }
+
+ @Test
+ public void abandonAndRestoreAllowedWithPermission() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
+ grant(Permission.ABANDON, project, "refs/heads/master", false, REGISTERED_USERS);
+ setApiUser(user);
+ gApi.changes().id(changeId).abandon();
+ assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED);
+ gApi.changes().id(changeId).restore();
+ assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
+ }
+
+ @Test
public void restore() throws Exception {
PushOneCommit.Result r = createChange();
String changeId = r.getChangeId();
@@ -298,6 +386,19 @@
}
@Test
+ public void restoreNotAllowedWithoutPermission() throws Exception {
+ PushOneCommit.Result r = createChange();
+ String changeId = r.getChangeId();
+ assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW);
+ gApi.changes().id(changeId).abandon();
+ setApiUser(user);
+ assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED);
+ exception.expect(AuthException.class);
+ exception.expectMessage("restore not permitted");
+ gApi.changes().id(changeId).restore();
+ }
+
+ @Test
public void revert() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
@@ -1603,7 +1704,7 @@
Iterables.getOnlyElement(query("project:{" + project.get() + "} owner:self")).changeId)
.isEqualTo(r.getChangeId());
setApiUser(user);
- assertThat(query("owner:self")).isEmpty();
+ assertThat(query("owner:self project:{" + project.get() + "}")).isEmpty();
}
@Test
@@ -1631,6 +1732,26 @@
}
@Test
+ public void editTopicWithoutPermissionNotAllowed() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
+ setApiUser(user);
+ exception.expect(AuthException.class);
+ exception.expectMessage("edit topic name not permitted");
+ gApi.changes().id(r.getChangeId()).topic("mytopic");
+ }
+
+ @Test
+ public void editTopicWithPermissionAllowed() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("");
+ grant(Permission.EDIT_TOPIC_NAME, project, "refs/heads/master", false, REGISTERED_USERS);
+ setApiUser(user);
+ gApi.changes().id(r.getChangeId()).topic("mytopic");
+ assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("mytopic");
+ }
+
+ @Test
public void submitted() throws Exception {
PushOneCommit.Result r = createChange();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
@@ -1657,6 +1778,26 @@
}
@Test
+ public void submitNotAllowedWithoutPermission() throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+ setApiUser(user);
+ exception.expect(AuthException.class);
+ exception.expectMessage("submit not permitted");
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+ }
+
+ @Test
+ public void submitAllowedWithPermission() throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
+ grant(Permission.SUBMIT, project, "refs/heads/master", false, REGISTERED_USERS);
+ setApiUser(user);
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+ assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(ChangeStatus.MERGED);
+ }
+
+ @Test
public void check() throws Exception {
// TODO(dborowitz): Re-enable when ConsistencyChecker supports NoteDb.
assume().that(notesMigration.enabled()).isFalse();
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 577634e..3a535ba 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
@@ -24,6 +24,7 @@
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
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;
@@ -38,6 +39,7 @@
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestProjectInput;
+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;
@@ -59,6 +61,7 @@
import com.google.gerrit.extensions.common.LabelInfo;
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;
@@ -282,6 +285,15 @@
}
@Test
+ public void voteNotAllowedWithoutPermission() throws Exception {
+ PushOneCommit.Result r = createChange();
+ setApiUser(user);
+ exception.expect(AuthException.class);
+ exception.expectMessage("is restricted");
+ gApi.changes().id(r.getChange().getId().get()).current().review(ReviewInput.approve());
+ }
+
+ @Test
public void deleteDraft() throws Exception {
PushOneCommit.Result r = createDraft();
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).delete();
@@ -326,6 +338,28 @@
}
@Test
+ public void cherryPickSetChangeId() throws Exception {
+ PushOneCommit.Result r = pushTo("refs/for/master");
+ CherryPickInput in = new CherryPickInput();
+ in.destination = "foo";
+ String id = "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbe3f";
+ in.message = "it goes to foo branch\n\nChange-Id: " + id;
+
+ gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
+ ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
+
+ assertThat(orig.get().messages).hasSize(1);
+ ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
+
+ ChangeInfo changeInfo = cherry.get();
+
+ // The cherry-pick honors the ChangeId specified in the input message:
+ RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
+ assertThat(revInfo).isNotNull();
+ assertThat(revInfo.commit.message).endsWith(id + "\n");
+ }
+
+ @Test
public void cherryPickwithNoTopic() throws Exception {
PushOneCommit.Result r = pushTo("refs/for/master");
CherryPickInput in = new CherryPickInput();
@@ -733,14 +767,36 @@
@Test
public void description() throws Exception {
PushOneCommit.Result r = createChange();
- assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description())
- .isEqualTo("");
+ assertDescription(r, "");
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
- assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description())
- .isEqualTo("test");
+ assertDescription(r, "test");
gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("");
+ assertDescription(r, "");
+ }
+
+ @Test
+ public void setDescriptionNotAllowedWithoutPermission() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertDescription(r, "");
+ setApiUser(user);
+ exception.expect(AuthException.class);
+ exception.expectMessage("edit description not permitted");
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
+ }
+
+ @Test
+ public void setDescriptionAllowedWithPermission() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertDescription(r, "");
+ grant(Permission.OWNER, project, "refs/heads/master", false, REGISTERED_USERS);
+ setApiUser(user);
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test");
+ assertDescription(r, "test");
+ }
+
+ private void assertDescription(PushOneCommit.Result r, String expected) throws Exception {
assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description())
- .isEqualTo("");
+ .isEqualTo(expected);
}
@Test
@@ -940,9 +996,9 @@
recommend(r.getChangeId());
// check if it's blocked to delete a vote on a non-current patch set.
+ setApiUser(admin);
exception.expect(MethodNotAllowedException.class);
exception.expectMessage("Cannot access on non-current patch set");
- setApiUser(admin);
gApi.changes()
.id(r.getChangeId())
.revision(r.getCommit().getName())
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 11df473..e525e96 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
@@ -16,34 +16,51 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.extensions.common.EditInfoSubject.assertThat;
import static com.google.gerrit.extensions.common.RobotCommentInfoSubject.assertThatList;
+import static java.util.stream.Collectors.toList;
+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.AcceptanceTestRequestScope;
+import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.FixReplacementInfo;
import com.google.gerrit.extensions.common.FixSuggestionInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.BinaryResultSubject;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import org.hamcrest.core.StringContains;
+import java.util.Objects;
+import java.util.Optional;
import org.junit.Before;
import org.junit.Test;
public class RobotCommentsIT extends AbstractDaemonTest {
+ private static final String FILE_NAME = "file_to_fix.txt";
+ private static final String FILE_NAME2 = "another_file_to_fix.txt";
+ private static final String FILE_CONTENT =
+ "First line\nSecond line\nThird line\nFourth line\nFifth line\nSixth line"
+ + "\nSeventh line\nEighth line\nNinth line\nTenth line\n";
+ private static final String FILE_CONTENT2 = "1st line\n2nd line\n3rd line\n";
+
private String changeId;
private FixReplacementInfo fixReplacementInfo;
private FixSuggestionInfo fixSuggestionInfo;
@@ -51,7 +68,14 @@
@Before
public void setUp() throws Exception {
- PushOneCommit.Result changeResult = createChange();
+ PushOneCommit push =
+ pushFactory.create(
+ db,
+ admin.getIdent(),
+ testRepo,
+ "Provide files which can be used for fixes",
+ ImmutableMap.of(FILE_NAME, FILE_CONTENT, FILE_NAME2, FILE_CONTENT2));
+ PushOneCommit.Result changeResult = push.to("refs/for/master");
changeId = changeResult.getChangeId();
fixReplacementInfo = createFixReplacementInfo();
@@ -151,6 +175,75 @@
}
@Test
+ public void hugeRobotCommentIsRejected() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ int defaultSizeLimit = 1024 * 1024;
+ int sizeOfRest = 451;
+ fixReplacementInfo.replacement = getStringFor(defaultSizeLimit - sizeOfRest + 1);
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("limit");
+ addRobotComment(changeId, withFixRobotCommentInput);
+ }
+
+ @Test
+ public void reasonablyLargeRobotCommentIsAccepted() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ int defaultSizeLimit = 1024 * 1024;
+ int sizeOfRest = 451;
+ fixReplacementInfo.replacement = getStringFor(defaultSizeLimit - sizeOfRest);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+ assertThat(robotCommentInfos).hasSize(1);
+ }
+
+ @Test
+ @GerritConfig(name = "change.robotCommentSizeLimit", value = "10k")
+ public void maximumAllowedSizeOfRobotCommentCanBeAdjusted() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ int sizeLimit = 10 * 1024;
+ fixReplacementInfo.replacement = getStringFor(sizeLimit);
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("limit");
+ addRobotComment(changeId, withFixRobotCommentInput);
+ }
+
+ @Test
+ @GerritConfig(name = "change.robotCommentSizeLimit", value = "0")
+ public void zeroForMaximumAllowedSizeOfRobotCommentRemovesRestriction() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ int defaultSizeLimit = 1024 * 1024;
+ fixReplacementInfo.replacement = getStringFor(defaultSizeLimit);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+ assertThat(robotCommentInfos).hasSize(1);
+ }
+
+ @Test
+ @GerritConfig(name = "change.robotCommentSizeLimit", value = "-1")
+ public void negativeValueForMaximumAllowedSizeOfRobotCommentRemovesRestriction()
+ throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ int defaultSizeLimit = 1024 * 1024;
+ fixReplacementInfo.replacement = getStringFor(defaultSizeLimit);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+ assertThat(robotCommentInfos).hasSize(1);
+ }
+
+ @Test
public void addedFixSuggestionCanBeRetrieved() throws Exception {
assume().that(notesMigration.enabled()).isTrue();
@@ -263,21 +356,6 @@
}
@Test
- public void pathOfFixReplacementMustReferToFileOfComment() throws Exception {
- assume().that(notesMigration.enabled()).isTrue();
-
- fixReplacementInfo.path = "anotherFile.txt";
-
- exception.expect(BadRequestException.class);
- exception.expectMessage(
- String.format(
- "Replacements may only be specified "
- + "for the file %s on which the robot comment was added",
- withFixRobotCommentInput.path));
- addRobotComment(changeId, withFixRobotCommentInput);
- }
-
- @Test
public void rangeOfFixReplacementIsAcceptedAsIs() throws Exception {
assume().that(notesMigration.enabled()).isTrue();
@@ -313,11 +391,115 @@
fixReplacementInfo.range = createRange(13, 9, 5, 10);
exception.expect(BadRequestException.class);
- exception.expectMessage(new StringContains("Range (13:9 - 5:10)"));
+ exception.expectMessage("Range (13:9 - 5:10)");
addRobotComment(changeId, withFixRobotCommentInput);
}
@Test
+ public void rangesOfFixReplacementsOfSameFixSuggestionForSameFileMayNotOverlap()
+ throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 1);
+ fixReplacementInfo1.replacement = "First modification\n";
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(3, 0, 4, 0);
+ fixReplacementInfo2.replacement = "Second modification\n";
+
+ FixSuggestionInfo fixSuggestionInfo =
+ createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
+ withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("overlap");
+ addRobotComment(changeId, withFixRobotCommentInput);
+ }
+
+ @Test
+ public void rangesOfFixReplacementsOfSameFixSuggestionForDifferentFileMayOverlap()
+ throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 1);
+ fixReplacementInfo1.replacement = "First modification\n";
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME2;
+ fixReplacementInfo2.range = createRange(3, 0, 4, 0);
+ fixReplacementInfo2.replacement = "Second modification\n";
+
+ FixSuggestionInfo fixSuggestionInfo =
+ createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
+ withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+ assertThatList(robotCommentInfos).onlyElement().fixSuggestions().hasSize(1);
+ }
+
+ @Test
+ public void rangesOfFixReplacementsOfDifferentFixSuggestionsForSameFileMayOverlap()
+ throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 1);
+ fixReplacementInfo1.replacement = "First modification\n";
+ FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(3, 0, 4, 0);
+ fixReplacementInfo2.replacement = "Second modification\n";
+ FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
+
+ withFixRobotCommentInput.fixSuggestions =
+ ImmutableList.of(fixSuggestionInfo1, fixSuggestionInfo2);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+ assertThatList(robotCommentInfos).onlyElement().fixSuggestions().hasSize(2);
+ }
+
+ @Test
+ public void fixReplacementsDoNotNeedToBeOrderedAccordingToRange() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 0);
+ fixReplacementInfo1.replacement = "First modification\n";
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(3, 0, 4, 0);
+ fixReplacementInfo2.replacement = "Second modification\n";
+
+ FixReplacementInfo fixReplacementInfo3 = new FixReplacementInfo();
+ fixReplacementInfo3.path = FILE_NAME;
+ fixReplacementInfo3.range = createRange(4, 0, 5, 0);
+ fixReplacementInfo3.replacement = "Third modification\n";
+
+ FixSuggestionInfo fixSuggestionInfo =
+ createFixSuggestionInfo(fixReplacementInfo2, fixReplacementInfo1, fixReplacementInfo3);
+ withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+ assertThatList(robotCommentInfos).onlyElement().onlyFixSuggestion().replacements().hasSize(3);
+ }
+
+ @Test
public void replacementStringOfFixReplacementIsAcceptedAsIs() throws Exception {
assume().that(notesMigration.enabled()).isTrue();
@@ -349,13 +531,490 @@
}
@Test
+ public void fixWithinALineCanBeApplied() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nSecond line\nTModified contentrd line\nFourth line\nFifth line\n"
+ + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
+ }
+
+ @Test
+ public void fixSpanningMultipleLinesCanBeApplied() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content\n5";
+ fixReplacementInfo.range = createRange(3, 2, 5, 3);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nSecond line\nThModified content\n5th line\nSixth line\nSeventh line\n"
+ + "Eighth line\nNinth line\nTenth line\n");
+ }
+
+ @Test
+ public void fixWithTwoCloseReplacementsOnSameFileCanBeApplied() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 0);
+ fixReplacementInfo1.replacement = "First modification\n";
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(3, 0, 4, 0);
+ fixReplacementInfo2.replacement = "Some other modified content\n";
+
+ FixSuggestionInfo fixSuggestionInfo =
+ createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
+ withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nFirst modification\nSome other modified content\nFourth line\nFifth line\n"
+ + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
+ }
+
+ @Test
+ public void twoFixesOnSameFileCanBeApplied() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 0);
+ fixReplacementInfo1.replacement = "First modification\n";
+ FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(8, 0, 9, 0);
+ fixReplacementInfo2.replacement = "Some other modified content\n";
+ FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
+
+ RobotCommentInput robotCommentInput1 = createRobotCommentInput(fixSuggestionInfo1);
+ RobotCommentInput robotCommentInput2 = createRobotCommentInput(fixSuggestionInfo2);
+ addRobotComment(changeId, robotCommentInput1);
+ addRobotComment(changeId, robotCommentInput2);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(1));
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n"
+ + "Seventh line\nSome other modified content\nNinth line\nTenth line\n");
+ }
+
+ @Test
+ public void twoConflictingFixesOnSameFileCannotBeApplied() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 1);
+ fixReplacementInfo1.replacement = "First modification\n";
+ FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(3, 0, 4, 0);
+ fixReplacementInfo2.replacement = "Some other modified content\n";
+ FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
+
+ RobotCommentInput robotCommentInput1 = createRobotCommentInput(fixSuggestionInfo1);
+ RobotCommentInput robotCommentInput2 = createRobotCommentInput(fixSuggestionInfo2);
+ addRobotComment(changeId, robotCommentInput1);
+ addRobotComment(changeId, robotCommentInput2);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage("merge");
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(1));
+ }
+
+ @Test
+ public void twoFixesOfSameRobotCommentCanBeApplied() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 0);
+ fixReplacementInfo1.replacement = "First modification\n";
+ FixSuggestionInfo fixSuggestionInfo1 = createFixSuggestionInfo(fixReplacementInfo1);
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME;
+ fixReplacementInfo2.range = createRange(8, 0, 9, 0);
+ fixReplacementInfo2.replacement = "Some other modified content\n";
+ FixSuggestionInfo fixSuggestionInfo2 = createFixSuggestionInfo(fixReplacementInfo2);
+
+ withFixRobotCommentInput.fixSuggestions =
+ ImmutableList.of(fixSuggestionInfo1, fixSuggestionInfo2);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(0));
+ gApi.changes().id(changeId).current().applyFix(fixIds.get(1));
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n"
+ + "Seventh line\nSome other modified content\nNinth line\nTenth line\n");
+ }
+
+ @Test
+ public void fixReferringToDifferentFileThanRobotCommentCanBeApplied() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ fixReplacementInfo.path = FILE_NAME2;
+ fixReplacementInfo.range = createRange(2, 0, 3, 0);
+ fixReplacementInfo.replacement = "Modified content\n";
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME2);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo("1st line\nModified content\n3rd line\n");
+ }
+
+ @Test
+ public void fixInvolvingTwoFilesCanBeApplied() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
+ fixReplacementInfo1.path = FILE_NAME;
+ fixReplacementInfo1.range = createRange(2, 0, 3, 0);
+ fixReplacementInfo1.replacement = "First modification\n";
+
+ FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
+ fixReplacementInfo2.path = FILE_NAME2;
+ fixReplacementInfo2.range = createRange(1, 0, 2, 0);
+ fixReplacementInfo2.replacement = "Different file modification\n";
+
+ FixSuggestionInfo fixSuggestionInfo =
+ createFixSuggestionInfo(fixReplacementInfo1, fixReplacementInfo2);
+ withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n"
+ + "Seventh line\nEighth line\nNinth line\nTenth line\n");
+ Optional<BinaryResult> file2 = gApi.changes().id(changeId).edit().getFile(FILE_NAME2);
+ BinaryResultSubject.assertThat(file2)
+ .value()
+ .asString()
+ .isEqualTo("Different file modification\n2nd line\n3rd line\n");
+ }
+
+ @Test
+ public void fixReferringToNonExistentFileCannotBeApplied() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ fixReplacementInfo.path = "a_non_existent_file.txt";
+ fixReplacementInfo.range = createRange(1, 0, 2, 0);
+ fixReplacementInfo.replacement = "Modified content\n";
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ exception.expect(ResourceNotFoundException.class);
+ gApi.changes().id(changeId).current().applyFix(fixId);
+ }
+
+ @Test
+ public void fixOnPreviousPatchSetWithoutChangeEditCannotBeApplied() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ // Remember patch set and add another one.
+ String previousRevision = gApi.changes().id(changeId).get().currentRevision;
+ amendChange(changeId);
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage("current");
+ gApi.changes().id(changeId).revision(previousRevision).applyFix(fixId);
+ }
+
+ @Test
+ public void fixOnPreviousPatchSetWithExistingChangeEditCanBeApplied() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ // Create an empty change edit.
+ gApi.changes().id(changeId).edit().create();
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ // Remember patch set and add another one.
+ String previousRevision = gApi.changes().id(changeId).get().currentRevision;
+ amendChange(changeId);
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ EditInfo editInfo = gApi.changes().id(changeId).revision(previousRevision).applyFix(fixId);
+
+ Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
+ BinaryResultSubject.assertThat(file)
+ .value()
+ .asString()
+ .isEqualTo(
+ "First line\nSecond line\nTModified contentrd line\nFourth line\nFifth line\n"
+ + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
+ assertThat(editInfo).baseRevision().isEqualTo(previousRevision);
+ }
+
+ @Test
+ public void fixOnCurrentPatchSetWithChangeEditOnPreviousPatchSetCannotBeApplied()
+ throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ // Create an empty change edit.
+ gApi.changes().id(changeId).edit().create();
+
+ // Add another patch set.
+ amendChange(changeId);
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ exception.expect(ResourceConflictException.class);
+ exception.expectMessage("based");
+ gApi.changes().id(changeId).current().applyFix(fixId);
+ }
+
+ @Test
+ public void fixDoesNotModifyCommitMessageOfChangeEdit() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ String changeEditCommitMessage = "This is the commit message of the change edit.\n";
+ gApi.changes().id(changeId).edit().modifyCommitMessage(changeEditCommitMessage);
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
+ assertThat(commitMessage).isEqualTo(changeEditCommitMessage);
+ }
+
+ @Test
+ public void applyingFixTwiceIsIdempotent() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ gApi.changes().id(changeId).current().applyFix(fixId);
+ String expectedEditCommit =
+ gApi.changes().id(changeId).edit().get().map(edit -> edit.commit.commit).orElse("");
+
+ // Apply the fix again.
+ gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<EditInfo> editInfo = gApi.changes().id(changeId).edit().get();
+ assertThat(editInfo).value().commit().commit().isEqualTo(expectedEditCommit);
+ }
+
+ @Test
+ public void nonExistentFixCannotBeApplied() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+ String nonExistentFixId = fixId + "_non-existent";
+
+ exception.expect(ResourceNotFoundException.class);
+ gApi.changes().id(changeId).current().applyFix(nonExistentFixId);
+ }
+
+ @Test
+ public void applyingFixReturnsEditInfoForCreatedChangeEdit() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<EditInfo> expectedEditInfo = gApi.changes().id(changeId).edit().get();
+ String expectedEditCommit = expectedEditInfo.map(edit -> edit.commit.commit).orElse("");
+ assertThat(editInfo).commit().commit().isEqualTo(expectedEditCommit);
+ String expectedBaseRevision = expectedEditInfo.map(edit -> edit.baseRevision).orElse("");
+ assertThat(editInfo).baseRevision().isEqualTo(expectedBaseRevision);
+ }
+
+ @Test
+ public void applyingFixOnTopOfChangeEditReturnsEditInfoForUpdatedChangeEdit() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ gApi.changes().id(changeId).edit().create();
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(fixId);
+
+ Optional<EditInfo> expectedEditInfo = gApi.changes().id(changeId).edit().get();
+ String expectedEditCommit = expectedEditInfo.map(edit -> edit.commit.commit).orElse("");
+ assertThat(editInfo).commit().commit().isEqualTo(expectedEditCommit);
+ String expectedBaseRevision = expectedEditInfo.map(edit -> edit.baseRevision).orElse("");
+ assertThat(editInfo).baseRevision().isEqualTo(expectedBaseRevision);
+ }
+
+ @Test
+ public void createdChangeEditIsBasedOnCurrentPatchSet() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+ String currentRevision = gApi.changes().id(changeId).get().currentRevision;
+
+ fixReplacementInfo.path = FILE_NAME;
+ fixReplacementInfo.replacement = "Modified content";
+ fixReplacementInfo.range = createRange(3, 1, 3, 3);
+
+ addRobotComment(changeId, withFixRobotCommentInput);
+ List<RobotCommentInfo> robotCommentInfos = getRobotComments();
+
+ List<String> fixIds = getFixIds(robotCommentInfos);
+ String fixId = Iterables.getOnlyElement(fixIds);
+
+ EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(fixId);
+
+ assertThat(editInfo).baseRevision().isEqualTo(currentRevision);
+ }
+
+ @Test
public void robotCommentsNotSupportedWithoutNoteDb() throws Exception {
assume().that(notesMigration.enabled()).isFalse();
RobotCommentInput in = createRobotCommentInput();
ReviewInput reviewInput = new ReviewInput();
Map<String, List<RobotCommentInput>> robotComments = new HashMap<>();
- robotComments.put(FILE_NAME, Collections.singletonList(in));
+ robotComments.put(in.path, ImmutableList.of(in));
reviewInput.robotComments = robotComments;
reviewInput.message = "comment test";
@@ -389,7 +1048,7 @@
}
}
- private RobotCommentInput createRobotCommentInputWithMandatoryFields() {
+ private static RobotCommentInput createRobotCommentInputWithMandatoryFields() {
RobotCommentInput in = new RobotCommentInput();
in.robotId = "happyRobot";
in.robotRunId = "1";
@@ -399,7 +1058,8 @@
return in;
}
- private RobotCommentInput createRobotCommentInput(FixSuggestionInfo... fixSuggestionInfos) {
+ private static RobotCommentInput createRobotCommentInput(
+ FixSuggestionInfo... fixSuggestionInfos) {
RobotCommentInput in = createRobotCommentInputWithMandatoryFields();
in.url = "http://www.happy-robot.com";
in.properties = new HashMap<>();
@@ -409,7 +1069,8 @@
return in;
}
- private FixSuggestionInfo createFixSuggestionInfo(FixReplacementInfo... fixReplacementInfos) {
+ private static FixSuggestionInfo createFixSuggestionInfo(
+ FixReplacementInfo... fixReplacementInfos) {
FixSuggestionInfo newFixSuggestionInfo = new FixSuggestionInfo();
newFixSuggestionInfo.fixId = "An ID which must be overwritten.";
newFixSuggestionInfo.description = "A description for a suggested fix.";
@@ -417,15 +1078,15 @@
return newFixSuggestionInfo;
}
- private FixReplacementInfo createFixReplacementInfo() {
+ private static FixReplacementInfo createFixReplacementInfo() {
FixReplacementInfo newFixReplacementInfo = new FixReplacementInfo();
newFixReplacementInfo.path = FILE_NAME;
newFixReplacementInfo.replacement = "some replacement code";
- newFixReplacementInfo.range = createRange(3, 12, 15, 4);
+ newFixReplacementInfo.range = createRange(3, 9, 8, 4);
return newFixReplacementInfo;
}
- private Comment.Range createRange(
+ private static Comment.Range createRange(
int startLine, int startCharacter, int endLine, int endCharacter) {
Comment.Range range = new Comment.Range();
range.startLine = startLine;
@@ -439,8 +1100,7 @@
throws Exception {
ReviewInput reviewInput = new ReviewInput();
reviewInput.robotComments =
- Collections.singletonMap(
- robotCommentInput.path, Collections.singletonList(robotCommentInput));
+ Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
reviewInput.message = "robot comment test";
gApi.changes().id(targetChangeId).current().review(reviewInput);
}
@@ -470,4 +1130,22 @@
assertThat(c.path).isNull();
}
}
+
+ private static String getStringFor(int numberOfBytes) {
+ char[] chars = new char[numberOfBytes];
+ // 'a' will require one byte even when mapped to a JSON string
+ Arrays.fill(chars, 'a');
+ return new String(chars);
+ }
+
+ private static List<String> getFixIds(List<RobotCommentInfo> robotComments) {
+ assertThatList(robotComments).isNotNull();
+ return robotComments
+ .stream()
+ .map(robotCommentInfo -> robotCommentInfo.fixSuggestions)
+ .filter(Objects::nonNull)
+ .flatMap(List::stream)
+ .map(fixSuggestionInfo -> fixSuggestionInfo.fixId)
+ .collect(toList());
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 31ca9df..0ed5d8d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -376,6 +376,38 @@
}
@Test
+ public void pushPrivateChange() throws Exception {
+ // Push a private change.
+ PushOneCommit.Result r = pushTo("refs/for/master%private");
+ r.assertOkStatus();
+ assertThat(r.getChange().change().isPrivate()).isTrue();
+
+ // Pushing a new patch set without --private doesn't remove the privacy flag from the change.
+ r = amendChange(r.getChangeId(), "refs/for/master");
+ r.assertOkStatus();
+ assertThat(r.getChange().change().isPrivate()).isTrue();
+
+ // Remove the privacy flag from the change.
+ r = amendChange(r.getChangeId(), "refs/for/master%remove-private");
+ r.assertOkStatus();
+ assertThat(r.getChange().change().isPrivate()).isFalse();
+
+ // Normal push: privacy flag is not added back.
+ r = amendChange(r.getChangeId(), "refs/for/master");
+ r.assertOkStatus();
+ assertThat(r.getChange().change().isPrivate()).isFalse();
+
+ // Make the change private again.
+ r = pushTo("refs/for/master%private");
+ r.assertOkStatus();
+ assertThat(r.getChange().change().isPrivate()).isTrue();
+
+ // Can't use --private and --remove-private together.
+ r = pushTo("refs/for/master%private,remove-private");
+ r.assertErrorStatus();
+ }
+
+ @Test
public void pushForMasterAsDraft() throws Exception {
// create draft by pushing to 'refs/drafts/'
PushOneCommit.Result r = pushTo("refs/drafts/master");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index b900cc7..ab04ba5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static java.util.stream.Collectors.toList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
@@ -54,6 +55,8 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.LsRemoteCommand;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
@@ -470,6 +473,46 @@
assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(c4, 1));
}
+ @Test
+ public void advertisedReferencesOmitPrivateChangesOfOtherUsers() throws Exception {
+ allow(Permission.READ, REGISTERED_USERS, "refs/heads/master");
+
+ TestRepository<?> userTestRepository = cloneProject(project, user);
+ try (Git git = userTestRepository.git()) {
+ LsRemoteCommand lsRemoteCommand = git.lsRemote();
+ String change3RefName = c3.currentPatchSet().getRefName();
+
+ List<String> initialRefNames =
+ lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
+ assertWithMessage("Precondition violated").that(initialRefNames).contains(change3RefName);
+
+ gApi.changes().id(c3.getId().get()).setPrivate(true);
+
+ List<String> refNames = lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
+ assertThat(refNames).doesNotContain(change3RefName);
+ }
+ }
+
+ @Test
+ public void advertisedReferencesIncludePrivateChangesWhenAllRefsMayBeRead() throws Exception {
+ allow(Permission.READ, REGISTERED_USERS, "refs/*");
+
+ TestRepository<?> userTestRepository = cloneProject(project, user);
+ try (Git git = userTestRepository.git()) {
+ LsRemoteCommand lsRemoteCommand = git.lsRemote();
+ String change3RefName = c3.currentPatchSet().getRefName();
+
+ List<String> initialRefNames =
+ lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
+ assertWithMessage("Precondition violated").that(initialRefNames).contains(change3RefName);
+
+ gApi.changes().id(c3.getId().get()).setPrivate(true);
+
+ List<String> refNames = lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
+ assertThat(refNames).contains(change3RefName);
+ }
+ }
+
/**
* Assert that refs seen by a non-admin user match expected.
*
@@ -500,7 +543,7 @@
throws Exception {
List<String> expected = new ArrayList<>(expectedWithMeta.length);
for (String r : expectedWithMeta) {
- if (notesMigration.writeChanges() || !r.endsWith(RefNames.META_SUFFIX)) {
+ if (notesMigration.commitChangeWrites() || !r.endsWith(RefNames.META_SUFFIX)) {
expected.add(r);
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index 06b8f68..d5d4620 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -16,7 +16,8 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.GitUtil.fetch;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static org.junit.Assert.fail;
@@ -24,17 +25,24 @@
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
+import com.google.common.collect.ImmutableList;
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.Sandboxed;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.common.AccountExternalIdInfo;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.account.ExternalId;
-import com.google.gerrit.server.account.ExternalIds;
-import com.google.gerrit.server.account.ExternalIdsUpdate;
+import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdReader;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate.RefsMetaExternalIdsUpdate;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.LockFailureException;
import com.google.gson.reflect.TypeToken;
@@ -44,25 +52,32 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Test;
@Sandboxed
public class ExternalIdIT extends AbstractDaemonTest {
@Inject private AllUsersName allUsers;
-
@Inject private ExternalIdsUpdate.Server extIdsUpdate;
-
@Inject private ExternalIds externalIds;
+ @Inject private ExternalIdReader externalIdReader;
+ @Inject private MetricMaker metricMaker;
@Test
- public void getExternalIDs() throws Exception {
+ public void getExternalIds() throws Exception {
Collection<ExternalId> expectedIds = accountCache.get(user.getId()).getExternalIds();
List<AccountExternalIdInfo> expectedIdInfos = new ArrayList<>();
@@ -89,7 +104,7 @@
}
@Test
- public void deleteExternalIDs() throws Exception {
+ public void deleteExternalIds() throws Exception {
setApiUser(user);
List<AccountExternalIdInfo> externalIds = gApi.accounts().self().getExternalIds();
@@ -115,7 +130,19 @@
}
@Test
- public void deleteExternalIDs_Conflict() throws Exception {
+ public void deleteExternalIdOfPreferredEmail() throws Exception {
+ String preferredEmail = gApi.accounts().self().get().email;
+ assertThat(preferredEmail).isNotNull();
+
+ gApi.accounts()
+ .self()
+ .deleteExternalIds(
+ ImmutableList.of(ExternalId.Key.create(SCHEME_MAILTO, preferredEmail).get()));
+ assertThat(gApi.accounts().self().get().email).isNull();
+ }
+
+ @Test
+ public void deleteExternalIds_Conflict() throws Exception {
List<String> toDelete = new ArrayList<>();
String externalIdStr = "username:" + user.username;
toDelete.add(externalIdStr);
@@ -126,7 +153,7 @@
}
@Test
- public void deleteExternalIDs_UnprocessableEntity() throws Exception {
+ public void deleteExternalIds_UnprocessableEntity() throws Exception {
List<String> toDelete = new ArrayList<>();
String externalIdStr = "mailto:user@domain.com";
toDelete.add(externalIdStr);
@@ -173,7 +200,7 @@
@Test
public void retryOnLockFailure() throws Exception {
- Retryer<Void> retryer =
+ Retryer<RefsMetaExternalIdsUpdate> retryer =
ExternalIdsUpdate.retryerBuilder()
.withBlockStrategy(
new BlockStrategy() {
@@ -192,6 +219,9 @@
new ExternalIdsUpdate(
repoManager,
allUsers,
+ metricMaker,
+ externalIds,
+ new DisabledExternalIdCache(),
serverIdent.get(),
serverIdent.get(),
() -> {
@@ -208,8 +238,8 @@
update.insert(db, ExternalId.create(fooId, admin.id));
assertThat(doneBgUpdate.get()).isTrue();
- assertThat(externalIds.get(fooId)).isNotNull();
- assertThat(externalIds.get(barId)).isNotNull();
+ assertThat(externalIds.get(db, fooId)).isNotNull();
+ assertThat(externalIds.get(db, barId)).isNotNull();
}
@Test
@@ -224,6 +254,9 @@
new ExternalIdsUpdate(
repoManager,
allUsers,
+ metricMaker,
+ externalIds,
+ new DisabledExternalIdCache(),
serverIdent.get(),
serverIdent.get(),
() -> {
@@ -235,7 +268,7 @@
// Ignore, the successful insertion of the external ID is asserted later
}
},
- RetryerBuilder.<Void>newBuilder()
+ RetryerBuilder.<RefsMetaExternalIdsUpdate>newBuilder()
.retryIfException(e -> e instanceof LockFailureException)
.withStopStrategy(StopStrategies.stopAfterAttempt(extIdsKeys.length))
.build());
@@ -248,7 +281,87 @@
}
assertThat(bgCounter.get()).isEqualTo(extIdsKeys.length);
for (ExternalId.Key extIdKey : extIdsKeys) {
- assertThat(externalIds.get(extIdKey)).isNotNull();
+ assertThat(externalIds.get(db, extIdKey)).isNotNull();
+ }
+ }
+
+ @Test
+ public void readExternalIdWithAccountIdThatCanBeExpressedInKiB() throws Exception {
+ ExternalId.Key extIdKey = ExternalId.Key.parse("foo:bar");
+ Account.Id accountId = new Account.Id(1024 * 100);
+ extIdsUpdate.create().insert(db, ExternalId.create(extIdKey, accountId));
+ ExternalId extId = externalIds.get(db, extIdKey);
+ assertThat(extId.accountId()).isEqualTo(accountId);
+ }
+
+ @Test
+ @GerritConfig(name = "user.readExternalIdsFromGit", value = "true")
+ public void checkNoReloadAfterUpdate() throws Exception {
+ Set<ExternalId> expectedExtIds = new HashSet<>(externalIds.byAccount(db, admin.id));
+ externalIdReader.setFailOnLoad(true);
+
+ // insert external ID
+ ExternalId extId = ExternalId.create("foo", "bar", admin.id);
+ extIdsUpdate.create().insert(db, extId);
+ expectedExtIds.add(extId);
+ assertThat(externalIds.byAccount(db, admin.id)).containsExactlyElementsIn(expectedExtIds);
+
+ // update external ID
+ expectedExtIds.remove(extId);
+ extId = ExternalId.createWithEmail("foo", "bar", admin.id, "foo.bar@example.com");
+ extIdsUpdate.create().upsert(db, extId);
+ expectedExtIds.add(extId);
+ assertThat(externalIds.byAccount(db, admin.id)).containsExactlyElementsIn(expectedExtIds);
+
+ // delete external ID
+ extIdsUpdate.create().delete(db, extId);
+ expectedExtIds.remove(extId);
+ assertThat(externalIds.byAccount(db, admin.id)).containsExactlyElementsIn(expectedExtIds);
+ }
+
+ @Test
+ @GerritConfig(name = "user.readExternalIdsFromGit", value = "true")
+ public void byAccountFailIfReadingExternalIdsFails() throws Exception {
+ externalIdReader.setFailOnLoad(true);
+
+ // update external ID branch so that external IDs need to be reloaded
+ insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id));
+
+ exception.expect(IOException.class);
+ externalIds.byAccount(db, admin.id);
+ }
+
+ @Test
+ @GerritConfig(name = "user.readExternalIdsFromGit", value = "true")
+ public void byEmailFailIfReadingExternalIdsFails() throws Exception {
+ externalIdReader.setFailOnLoad(true);
+
+ // update external ID branch so that external IDs need to be reloaded
+ insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id));
+
+ exception.expect(IOException.class);
+ externalIds.byEmail(db, admin.email);
+ }
+
+ @Test
+ @GerritConfig(name = "user.readExternalIdsFromGit", value = "true")
+ public void byAccountUpdateExternalIdsBehindGerritsBack() throws Exception {
+ Set<ExternalId> expectedExternalIds = new HashSet<>(externalIds.byAccount(db, admin.id));
+ ExternalId newExtId = ExternalId.create("foo", "bar", admin.id);
+ insertExtIdBehindGerritsBack(newExtId);
+ expectedExternalIds.add(newExtId);
+ assertThat(externalIds.byAccount(db, admin.id)).containsExactlyElementsIn(expectedExternalIds);
+ }
+
+ private void insertExtIdBehindGerritsBack(ExternalId extId) throws Exception {
+ try (Repository repo = repoManager.openRepository(allUsers);
+ RevWalk rw = new RevWalk(repo);
+ ObjectInserter ins = repo.newObjectInserter()) {
+ ObjectId rev = ExternalIdReader.readRevision(repo);
+ NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
+ ExternalIdsUpdate.insert(rw, ins, noteMap, extId);
+ ExternalIdsUpdate.commit(
+ repo, rw, ins, rev, noteMap, "insert new ID", serverIdent.get(), serverIdent.get());
}
}
}
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
index c69391c..82eae1b 100644
--- 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
@@ -39,8 +39,10 @@
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.ListChangesOption;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.GroupInfo;
@@ -62,6 +64,7 @@
import com.google.gerrit.server.project.Util;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
+import java.util.EnumSet;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.junit.After;
@@ -375,7 +378,7 @@
SubmitInput in = new SubmitInput();
in.onBehalfOf = admin2.email;
exception.expect(AuthException.class);
- exception.expectMessage("submit on behalf of not permitted");
+ exception.expectMessage("submit as not permitted");
gApi.changes().id(project.get() + "~master~" + r.getChangeId()).current().submit(in);
}
@@ -390,7 +393,7 @@
SubmitInput in = new SubmitInput();
in.onBehalfOf = user.email;
exception.expect(UnprocessableEntityException.class);
- exception.expectMessage("on_behalf_of account " + user.id + " cannot see destination ref");
+ exception.expectMessage("on_behalf_of account " + user.id + " cannot see change");
gApi.changes().id(changeId).current().submit(in);
}
@@ -529,6 +532,28 @@
assertThat(m.getRealAuthor()).isEqualTo(admin.id); // not user2
}
+ @Test
+ public void changeMessageCreatedOnBehalfOfHasRealUser() throws Exception {
+ allowCodeReviewOnBehalfOf();
+
+ PushOneCommit.Result r = createChange();
+ ReviewInput in = new ReviewInput();
+ in.onBehalfOf = user.id.toString();
+ in.message = "Message on behalf of";
+ in.label("Code-Review", 1);
+
+ setApiUser(accounts.user2());
+ gApi.changes().id(r.getChangeId()).revision(r.getPatchSetId().getId()).review(in);
+
+ ChangeInfo info =
+ gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.MESSAGES));
+ assertThat(info.messages).hasSize(2);
+
+ ChangeMessageInfo changeMessageInfo = Iterables.getLast(info.messages);
+ assertThat(changeMessageInfo.realAuthor).isNotNull();
+ assertThat(changeMessageInfo.realAuthor._accountId).isEqualTo(accounts.user2().id.get());
+ }
+
private void allowCodeReviewOnBehalfOf() throws Exception {
ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
LabelType codeReviewType = Util.codeReview();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
index 9378591..17dabde 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
@@ -18,23 +18,16 @@
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.PutUsername;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import java.util.Collections;
import org.junit.Test;
public class PutUsernameIT extends AbstractDaemonTest {
- @Inject private SchemaFactory<ReviewDb> reviewDbProvider;
-
@Test
public void set() throws Exception {
PutUsername.Input in = new PutUsername.Input();
in.username = "myUsername";
- RestResponse r = adminRestSession.put("/accounts/" + createUser().get() + "/username", in);
+ RestResponse r =
+ adminRestSession.put("/accounts/" + accounts.create().id.get() + "/username", in);
r.assertOK();
assertThat(newGson().fromJson(r.getReader(), String.class)).isEqualTo(in.username);
}
@@ -43,7 +36,9 @@
public void setExisting_Conflict() throws Exception {
PutUsername.Input in = new PutUsername.Input();
in.username = admin.username;
- adminRestSession.put("/accounts/" + createUser().get() + "/username", in).assertConflict();
+ adminRestSession
+ .put("/accounts/" + accounts.create().id.get() + "/username", in)
+ .assertConflict();
}
@Test
@@ -57,13 +52,4 @@
public void delete_MethodNotAllowed() throws Exception {
adminRestSession.put("/accounts/" + admin.username + "/username").assertMethodNotAllowed();
}
-
- private Account.Id createUser() throws Exception {
- try (ReviewDb db = reviewDbProvider.open()) {
- Account.Id id = new Account.Id(db.nextAccountId());
- Account a = new Account(id, TimeUtil.nowTs());
- db.accounts().insert(Collections.singleton(a));
- return id;
- }
- }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
index 35ba1a2..a809df9 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -16,19 +16,26 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.Sandboxed;
+import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.changes.AssigneeInput;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.testutil.FakeEmailSender.Message;
import com.google.gerrit.testutil.TestTimeUtil;
import java.util.Iterator;
import java.util.List;
+import org.eclipse.jgit.transport.RefSpec;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
@@ -124,6 +131,43 @@
assertThat(deleteAssignee(r)).isNull();
}
+ @Test
+ @Sandboxed
+ public void setAssigneeToInactiveUser() throws Exception {
+ PushOneCommit.Result r = createChange();
+ gApi.accounts().id(user.getId().get()).setActive(false);
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("is not active");
+ setAssignee(r, user.email);
+ }
+
+ @Test
+ public void setAssigneeForNonVisibleChange() throws Exception {
+ git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
+ testRepo.reset(RefNames.REFS_CONFIG);
+ PushOneCommit.Result r = createChange("refs/for/refs/meta/config");
+ exception.expect(AuthException.class);
+ exception.expectMessage("read not permitted");
+ setAssignee(r, user.email);
+ }
+
+ @Test
+ public void setAssigneeNotAllowedWithoutPermission() throws Exception {
+ PushOneCommit.Result r = createChange();
+ setApiUser(user);
+ exception.expect(AuthException.class);
+ exception.expectMessage("not permitted");
+ setAssignee(r, user.email);
+ }
+
+ @Test
+ public void setAssigneeAllowedWithPermission() throws Exception {
+ PushOneCommit.Result r = createChange();
+ grant(Permission.EDIT_ASSIGNEE, project, "refs/heads/master", false, REGISTERED_USERS);
+ setApiUser(user);
+ assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get());
+ }
+
private AccountInfo getAssignee(PushOneCommit.Result r) throws Exception {
return gApi.changes().id(r.getChange().getId().get()).getAssignee();
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
new file mode 100644
index 0000000..d8c00cf
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -0,0 +1,275 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.TruthJUnit.assume;
+
+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.NoHttpd;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.changes.AddReviewerInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
+import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.testutil.FakeEmailSender.Message;
+import java.util.EnumSet;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+@NoHttpd
+public class ChangeReviewersByEmailIT extends AbstractDaemonTest {
+
+ @Before
+ public void setUp() throws Exception {
+ ConfigInput conf = new ConfigInput();
+ conf.enableReviewerByEmail = InheritableBoolean.TRUE;
+ gApi.projects().name(project.get()).config(conf);
+ }
+
+ @Test
+ public void addByEmail() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+
+ for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = toRfcAddressString(acc);
+ input.state = state;
+ gApi.changes().id(r.getChangeId()).addReviewer(input);
+
+ ChangeInfo info =
+ gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+ assertThat(info.reviewers).isEqualTo(ImmutableMap.of(state, ImmutableList.of(acc)));
+ // All reviewers added by email should be removable
+ assertThat(info.removableReviewers).isEqualTo(ImmutableList.of(acc));
+ }
+ }
+
+ @Test
+ public void removeByEmail() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+
+ for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput addInput = new AddReviewerInput();
+ addInput.reviewer = toRfcAddressString(acc);
+ addInput.state = state;
+ gApi.changes().id(r.getChangeId()).addReviewer(addInput);
+
+ gApi.changes().id(r.getChangeId()).reviewer(acc.email).remove();
+
+ ChangeInfo info =
+ gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+ assertThat(info.reviewers).isEmpty();
+ }
+ }
+
+ @Test
+ public void convertFromCCToReviewer() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput addInput = new AddReviewerInput();
+ addInput.reviewer = toRfcAddressString(acc);
+ addInput.state = ReviewerState.CC;
+ gApi.changes().id(r.getChangeId()).addReviewer(addInput);
+
+ AddReviewerInput modifyInput = new AddReviewerInput();
+ modifyInput.reviewer = addInput.reviewer;
+ modifyInput.state = ReviewerState.REVIEWER;
+ gApi.changes().id(r.getChangeId()).addReviewer(modifyInput);
+
+ ChangeInfo info =
+ gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS));
+ assertThat(info.reviewers)
+ .isEqualTo(ImmutableMap.of(ReviewerState.REVIEWER, ImmutableList.of(acc)));
+ }
+
+ @Test
+ public void addedReviewersGetNotified() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+
+ for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = toRfcAddressString(acc);
+ input.state = state;
+ gApi.changes().id(r.getChangeId()).addReviewer(input);
+
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ assertThat(messages.get(0).rcpt()).containsExactly(Address.parse(input.reviewer));
+ sender.clear();
+ }
+ }
+
+ @Test
+ public void removingReviewerTriggersNotification() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+
+ for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput addInput = new AddReviewerInput();
+ addInput.reviewer = toRfcAddressString(acc);
+ addInput.state = state;
+ gApi.changes().id(r.getChangeId()).addReviewer(addInput);
+
+ // Review change as user
+ ReviewInput reviewInput = new ReviewInput();
+ reviewInput.message = "I have a comment";
+ setApiUser(user);
+ revision(r).review(reviewInput);
+ setApiUser(admin);
+
+ sender.clear();
+
+ // Delete as admin
+ gApi.changes().id(r.getChangeId()).reviewer(addInput.reviewer).remove();
+
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ assertThat(messages.get(0).rcpt())
+ .containsExactly(Address.parse(addInput.reviewer), user.emailAddress);
+ sender.clear();
+ }
+ }
+
+ @Test
+ public void reviewerAndCCReceiveRegularNotification() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+
+ for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = toRfcAddressString(acc);
+ input.state = state;
+ gApi.changes().id(r.getChangeId()).addReviewer(input);
+ sender.clear();
+
+ gApi.changes()
+ .id(r.getChangeId())
+ .revision(r.getCommit().name())
+ .review(ReviewInput.approve());
+
+ if (state == ReviewerState.CC) {
+ assertNotifyCc(Address.parse(input.reviewer));
+ } else {
+ assertNotifyTo(Address.parse(input.reviewer));
+ }
+ }
+ }
+
+ @Test
+ public void rejectMissingEmail() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ PushOneCommit.Result r = createChange();
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("email invalid");
+ gApi.changes().id(r.getChangeId()).addReviewer("");
+ }
+
+ @Test
+ public void rejectMalformedEmail() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ PushOneCommit.Result r = createChange();
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage("email invalid");
+ gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@");
+ }
+
+ @Test
+ public void rejectOnNonPublicChange() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+ PushOneCommit.Result r = createDraftChange();
+
+ exception.expect(BadRequestException.class);
+ exception.expectMessage("change is not publicly visible");
+ gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@gerritcodereview.com>");
+ }
+
+ @Test
+ public void rejectWhenFeatureIsDisabled() throws Exception {
+ assume().that(notesMigration.readChanges()).isTrue();
+
+ ConfigInput conf = new ConfigInput();
+ conf.enableReviewerByEmail = InheritableBoolean.FALSE;
+ gApi.projects().name(project.get()).config(conf);
+
+ PushOneCommit.Result r = createChange();
+
+ exception.expect(UnprocessableEntityException.class);
+ exception.expectMessage(
+ "Foo Bar <foo.bar@gerritcodereview.com> does not identify a registered user or group");
+ gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@gerritcodereview.com>");
+ }
+
+ @Test
+ public void reviewersByEmailAreServedFromIndex() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+ AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com");
+
+ for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) {
+ PushOneCommit.Result r = createChange();
+
+ AddReviewerInput input = new AddReviewerInput();
+ input.reviewer = toRfcAddressString(acc);
+ input.state = state;
+ gApi.changes().id(r.getChangeId()).addReviewer(input);
+
+ notesMigration.setFailOnLoad(true);
+ try {
+ ChangeInfo info =
+ Iterables.getOnlyElement(
+ gApi.changes()
+ .query(r.getChangeId())
+ .withOption(ListChangesOption.DETAILED_LABELS)
+ .get());
+ assertThat(info.reviewers).isEqualTo(ImmutableMap.of(state, ImmutableList.of(acc)));
+ } finally {
+ notesMigration.setFailOnLoad(false);
+ }
+ }
+ }
+
+ private static String toRfcAddressString(AccountInfo info) {
+ return (new Address(info.name, info.email)).toString();
+ }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 66966c3..846c580 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -16,6 +16,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.extensions.client.ReviewerState.CC;
import static com.google.gerrit.extensions.client.ReviewerState.REMOVED;
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
@@ -42,6 +43,7 @@
import com.google.gson.stream.JsonReader;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -655,6 +657,30 @@
assertThat(reviewerResult.ccs).hasSize(1);
}
+ @Test
+ public void removingReviewerRemovesTheirVote() throws Exception {
+ String crLabel = "Code-Review";
+ PushOneCommit.Result r = createChange();
+ ReviewInput input = ReviewInput.approve().reviewer(admin.email);
+ ReviewResult addResult = review(r.getChangeId(), r.getCommit().name(), input);
+ assertThat(addResult.reviewers).isNotNull();
+ assertThat(addResult.reviewers).hasSize(1);
+
+ Map<String, LabelInfo> changeLabels = getChangeLabels(r.getChangeId());
+ assertThat(changeLabels.get(crLabel).all).hasSize(1);
+
+ RestResponse deleteResult = deleteReviewer(r.getChangeId(), admin);
+ deleteResult.assertNoContent();
+
+ changeLabels = getChangeLabels(r.getChangeId());
+ assertThat(changeLabels.get(crLabel).all).isNull();
+
+ // Check that the vote is gone even after the reviewer is added back
+ addReviewer(r.getChangeId(), admin.email);
+ changeLabels = getChangeLabels(r.getChangeId());
+ assertThat(changeLabels.get(crLabel).all).isNull();
+ }
+
private AddReviewerResult addReviewer(String changeId, String reviewer) throws Exception {
return addReviewer(changeId, reviewer, SC_OK);
}
@@ -735,4 +761,8 @@
}
return result;
}
+
+ private Map<String, LabelInfo> getChangeLabels(String changeId) throws Exception {
+ return gApi.changes().id(changeId).get(EnumSet.of(DETAILED_LABELS)).labels;
+ }
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index f79b5fa..146b5ca 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -23,6 +23,7 @@
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.RestResponse;
@@ -30,11 +31,15 @@
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.MergeInput;
+import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -45,10 +50,13 @@
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.FakeEmailSender.Message;
import com.google.gerrit.testutil.TestTimeUtil;
+import java.util.Iterator;
import java.util.List;
+import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -278,6 +286,79 @@
assertCreateSucceeds(in);
}
+ @Test
+ public void cherryPickCommitWithoutChangeId() throws Exception {
+ // This test is a little superfluous, since the current cherry-pick code ignores
+ // the commit message of the to-be-cherry-picked change, using the one in
+ // CherryPickInput instead.
+ CherryPickInput input = new CherryPickInput();
+ input.destination = "foo";
+ input.message = "it goes to foo branch";
+ gApi.projects().name(project.get()).branch(input.destination).create(new BranchInput());
+
+ RevCommit revCommit = createNewCommitWithoutChangeId();
+ ChangeInfo changeInfo =
+ gApi.projects().name(project.get()).commit(revCommit.getName()).cherryPick(input).get();
+
+ assertThat(changeInfo.messages).hasSize(1);
+ Iterator<ChangeMessageInfo> messageIterator = changeInfo.messages.iterator();
+ String expectedMessage =
+ String.format("Patch Set 1: Cherry Picked from commit %s.", revCommit.getName());
+ assertThat(messageIterator.next().message).isEqualTo(expectedMessage);
+
+ RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
+ assertThat(revInfo).isNotNull();
+ CommitInfo commitInfo = revInfo.commit;
+ assertThat(commitInfo.message)
+ .isEqualTo(input.message + "\n\nChange-Id: " + changeInfo.changeId + "\n");
+ }
+
+ @Test
+ public void cherryPickCommitWithChangeId() throws Exception {
+ CherryPickInput input = new CherryPickInput();
+ input.destination = "foo";
+
+ RevCommit revCommit = createChange().getCommit();
+ List<String> footers = revCommit.getFooterLines("Change-Id");
+ assertThat(footers).hasSize(1);
+ String changeId = footers.get(0);
+
+ input.message = "it goes to foo branch\n\nChange-Id: " + changeId;
+ gApi.projects().name(project.get()).branch(input.destination).create(new BranchInput());
+
+ ChangeInfo changeInfo =
+ gApi.projects().name(project.get()).commit(revCommit.getName()).cherryPick(input).get();
+
+ assertThat(changeInfo.messages).hasSize(1);
+ Iterator<ChangeMessageInfo> messageIterator = changeInfo.messages.iterator();
+ String expectedMessage =
+ String.format("Patch Set 1: Cherry Picked from commit %s.", revCommit.getName());
+ assertThat(messageIterator.next().message).isEqualTo(expectedMessage);
+
+ RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
+ assertThat(revInfo).isNotNull();
+ assertThat(revInfo.commit.message).isEqualTo(input.message + "\n");
+ }
+
+ private RevCommit createNewCommitWithoutChangeId() throws Exception {
+ try (Repository repo = repoManager.openRepository(project);
+ RevWalk walk = new RevWalk(repo)) {
+ Ref ref = repo.exactRef("refs/heads/master");
+ RevCommit tip = null;
+ if (ref != null) {
+ tip = walk.parseCommit(ref.getObjectId());
+ }
+ TestRepository<?> testSrcRepo = new TestRepository<>(repo);
+ TestRepository<?>.BranchBuilder builder = testSrcRepo.branch("refs/heads/master");
+ RevCommit revCommit =
+ tip == null
+ ? builder.commit().message("commit 1").add("a.txt", "content").create()
+ : builder.commit().parent(tip).message("commit 1").add("a.txt", "content").create();
+ assertThat(GitUtil.getChangeId(testSrcRepo, revCommit).isPresent()).isFalse();
+ return revCommit;
+ }
+ }
+
private ChangeInput newChangeInput(ChangeStatus status) {
ChangeInput in = new ChangeInput();
in.project = project.get();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
index 244efbf..228b478 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
@@ -92,7 +92,7 @@
din.message = "comment on a.txt";
gApi.changes().id(changeId).current().createDraft(din);
- if (notesMigration.writeChanges()) {
+ if (notesMigration.commitChangeWrites()) {
assertThat(getDraftRef(admin, id)).isNotNull();
}
@@ -110,7 +110,7 @@
deletePatchSet(changeId, ps);
assertThat(queryProvider.get().byKeyPrefix(changeId)).isEmpty();
- if (notesMigration.writeChanges()) {
+ if (notesMigration.commitChangeWrites()) {
assertThat(getDraftRef(admin, id)).isNull();
assertThat(getMetaRef(id)).isNull();
}
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 18925b4..d8b7a52 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
@@ -252,7 +252,7 @@
PushOneCommit.Result r = createChange();
setApiUser(user);
exception.expect(AuthException.class);
- exception.expectMessage("Editing hashtags not permitted");
+ exception.expectMessage("edit hashtags not permitted");
addHashtags(r, "MyHashtag");
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 8d9885c..b4f68fa 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -586,7 +586,7 @@
public boolean updateChange(ChangeContext ctx) throws OrmException {
PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId);
psUtil.setGroups(ctx.getDb(), ctx.getUpdate(psId), ps, ImmutableList.<String>of());
- ctx.bumpLastUpdatedOn(false);
+ ctx.dontBumpLastUpdatedOn();
return true;
}
});
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
index 688a8e9..c6a94b2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -23,7 +23,6 @@
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.acceptance.TestAccount;
-import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.StarsInput;
@@ -39,7 +38,6 @@
import com.google.gerrit.server.mail.Address;
import com.google.gerrit.testutil.FakeEmailSender.Message;
import com.google.inject.Inject;
-import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
@@ -148,6 +146,60 @@
}
@Test
+ public void noNotificationForPrivateChangesForWatchersInNotifyConfig() throws Exception {
+ Address addr = new Address("Watcher", "watcher@example.com");
+ NotifyConfig nc = new NotifyConfig();
+ nc.addEmail(addr);
+ nc.setName("team");
+ nc.setHeader(NotifyConfig.Header.TO);
+ nc.setTypes(EnumSet.of(NotifyType.NEW_CHANGES));
+
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ cfg.putNotifyConfig("team", nc);
+ saveProjectConfig(project, cfg);
+
+ sender.clear();
+ PushOneCommit.Result r =
+ pushFactory
+ .create(db, admin.getIdent(), testRepo, "private change", "a", "a1")
+ .to("refs/for/master%private");
+ r.assertOkStatus();
+
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
+ @Test
+ public void noNotificationForChangeThatIsTurnedPrivateForWatchersInNotifyConfig()
+ throws Exception {
+ Address addr = new Address("Watcher", "watcher@example.com");
+ NotifyConfig nc = new NotifyConfig();
+ nc.addEmail(addr);
+ nc.setName("team");
+ nc.setHeader(NotifyConfig.Header.TO);
+ nc.setTypes(EnumSet.of(NotifyType.NEW_PATCHSETS));
+
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ cfg.putNotifyConfig("team", nc);
+ saveProjectConfig(project, cfg);
+
+ PushOneCommit.Result r =
+ pushFactory
+ .create(db, admin.getIdent(), testRepo, "subject", "a", "a1")
+ .to("refs/for/master");
+ r.assertOkStatus();
+
+ sender.clear();
+
+ r =
+ pushFactory
+ .create(db, admin.getIdent(), testRepo, "subject", "a", "a2", r.getChangeId())
+ .to("refs/for/master%private");
+ r.assertOkStatus();
+
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
+ @Test
public void watchProject() throws Exception {
// watch project
String watchedProject = createProject("watchedProject").get();
@@ -506,9 +558,7 @@
@Test
public void deleteAllProjectWatchesIfWatchConfigIsTheOnlyFileInUserBranch() throws Exception {
// Create account that has no files in its refs/users/ branch.
- Account.Id id = new Account.Id(db.nextAccountId());
- Account a = new Account(id, TimeUtil.nowTs());
- db.accounts().insert(Collections.singleton(a));
+ Account.Id id = accounts.create().id;
// Add a project watch so that a watch.config file in the refs/users/ branch is created.
Map<ProjectWatchKey, Set<NotifyType>> watches = new HashMap<>();
@@ -521,4 +571,69 @@
watchConfig.deleteAllProjectWatches(id);
assertThat(watchConfig.getProjectWatches(id)).isEmpty();
}
+
+ @Test
+ public void watchProjectNoNotificationForPrivateChange() throws Exception {
+ // watch project
+ String watchedProject = createProject("watchedProject").get();
+ setApiUser(user);
+ watch(watchedProject, null);
+
+ // push a private change to watched project -> should not trigger email notification
+ setApiUser(admin);
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(new Project.NameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(db, admin.getIdent(), watchedRepo, "private change", "a", "a1")
+ .to("refs/for/master%private");
+ r.assertOkStatus();
+
+ // assert email notification
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
+ @Test
+ public void watchProjectNotifyOnPrivateChange() throws Exception {
+ String watchedProject = createProject("watchedProject").get();
+
+ // create group that can view all private changes
+ GroupInfo groupThatCanViewPrivateChanges =
+ gApi.groups().create("groupThatCanViewPrivateChanges").get();
+ grant(
+ Permission.VIEW_PRIVATE_CHANGES,
+ new Project.NameKey(watchedProject),
+ "refs/*",
+ false,
+ new AccountGroup.UUID(groupThatCanViewPrivateChanges.id));
+
+ // watch project as user that can't view private changes
+ setApiUser(user);
+ watch(watchedProject, null);
+
+ // watch project as user that can view all private change
+ TestAccount userThatCanViewPrivateChanges =
+ accounts.create("user2", "user2@test.com", "User2", groupThatCanViewPrivateChanges.name);
+ setApiUser(userThatCanViewPrivateChanges);
+ watch(watchedProject, null);
+
+ // push a private change to watched project -> should trigger email notification for
+ // userThatCanViewPrivateChanges, but not for user
+ setApiUser(admin);
+ TestRepository<InMemoryRepository> watchedRepo =
+ cloneProject(new Project.NameKey(watchedProject), admin);
+ PushOneCommit.Result r =
+ pushFactory
+ .create(db, admin.getIdent(), watchedRepo, "TRIGGER", "a", "a1")
+ .to("refs/for/master%private");
+ r.assertOkStatus();
+
+ // assert email notification
+ List<Message> messages = sender.getMessages();
+ assertThat(messages).hasSize(1);
+ Message m = messages.get(0);
+ assertThat(m.rcpt()).containsExactly(userThatCanViewPrivateChanges.emailAddress);
+ assertThat(m.body()).contains("Change subject: TRIGGER\n");
+ assertThat(m.body()).contains("Gerrit-PatchSet: 1\n");
+ }
}
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 3b86c95..8bdc410 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -131,7 +131,22 @@
@Override
public V get(K key, Callable<? extends V> valueLoader) throws ExecutionException {
- return mem.get(key, new LoadingCallable(key, valueLoader)).value;
+ return mem.get(
+ key,
+ () -> {
+ if (store.mightContain(key)) {
+ ValueHolder<V> h = store.getIfPresent(key);
+ if (h != null) {
+ return h;
+ }
+ }
+
+ ValueHolder<V> h = new ValueHolder<>(valueLoader.call());
+ h.created = TimeUtil.nowMs();
+ executor.execute(() -> store.put(key, h));
+ return h;
+ })
+ .value;
}
@Override
@@ -239,31 +254,6 @@
}
}
- private class LoadingCallable implements Callable<ValueHolder<V>> {
- private final K key;
- private final Callable<? extends V> loader;
-
- LoadingCallable(K key, Callable<? extends V> loader) {
- this.key = key;
- this.loader = loader;
- }
-
- @Override
- public ValueHolder<V> call() throws Exception {
- if (store.mightContain(key)) {
- ValueHolder<V> h = store.getIfPresent(key);
- if (h != null) {
- return h;
- }
- }
-
- final ValueHolder<V> h = new ValueHolder<>(loader.call());
- h.created = TimeUtil.nowMs();
- executor.execute(() -> store.put(key, h));
- return h;
- }
- }
-
private static class KeyType<K> {
String columnType() {
return "OTHER";
diff --git a/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java b/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 15e0de0..80bca6d 100644
--- a/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -24,7 +24,6 @@
import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore;
import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder;
import com.google.inject.TypeLiteral;
-import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Before;
@@ -54,12 +53,9 @@
assertTrue(
impl.get(
"foo",
- new Callable<Boolean>() {
- @Override
- public Boolean call() throws Exception {
- called.set(true);
- return true;
- }
+ () -> {
+ called.set(true);
+ return true;
}));
assertTrue("used Callable", called.get());
assertTrue("exists in cache", impl.getIfPresent("foo"));
@@ -70,12 +66,9 @@
assertTrue(
impl.get(
"foo",
- new Callable<Boolean>() {
- @Override
- public Boolean call() throws Exception {
- called.set(true);
- return true;
- }
+ () -> {
+ called.set(true);
+ return true;
}));
assertFalse("did not invoke Callable", called.get());
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index 47c5224..6222c1b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -47,6 +47,7 @@
public static final String SUBMIT = "submit";
public static final String SUBMIT_AS = "submitAs";
public static final String VIEW_DRAFTS = "viewDrafts";
+ public static final String VIEW_PRIVATE_CHANGES = "viewPrivateChanges";
private static final List<String> NAMES_LC;
private static final int LABEL_INDEX;
@@ -74,6 +75,7 @@
NAMES_LC.add(SUBMIT.toLowerCase());
NAMES_LC.add(SUBMIT_AS.toLowerCase());
NAMES_LC.add(VIEW_DRAFTS.toLowerCase());
+ NAMES_LC.add(VIEW_PRIVATE_CHANGES.toLowerCase());
NAMES_LC.add(EDIT_TOPIC_NAME.toLowerCase());
NAMES_LC.add(EDIT_HASHTAGS.toLowerCase());
NAMES_LC.add(EDIT_ASSIGNEE.toLowerCase());
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
index 0f8ec09..7da2873 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -38,9 +38,9 @@
import com.google.gson.JsonObject;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import io.searchbox.client.JestResult;
import io.searchbox.core.Bulk;
import io.searchbox.core.Bulk.Builder;
@@ -76,7 +76,7 @@
private final AccountMapping mapping;
private final Provider<AccountCache> accountCache;
- @AssistedInject
+ @Inject
ElasticAccountIndex(
@GerritServerConfig Config cfg,
SitePaths sitePaths,
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 77cae9b..fdc14d7 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -34,6 +34,7 @@
import com.google.gerrit.reviewdb.client.Change.Id;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
@@ -54,9 +55,9 @@
import com.google.gson.JsonObject;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import io.searchbox.client.JestResult;
import io.searchbox.core.Bulk;
import io.searchbox.core.Bulk.Builder;
@@ -99,7 +100,7 @@
private final Provider<ReviewDb> db;
private final ChangeData.Factory changeDataFactory;
- @AssistedInject
+ @Inject
ElasticChangeIndex(
@GerritServerConfig Config cfg,
Provider<ReviewDb> db,
@@ -342,6 +343,16 @@
cd.setReviewers(ReviewerSet.empty());
}
+ if (source.get(ChangeField.REVIEWER_BY_EMAIL.getName()) != null) {
+ cd.setReviewersByEmail(
+ ChangeField.parseReviewerByEmailFieldValues(
+ FluentIterable.from(
+ source.get(ChangeField.REVIEWER_BY_EMAIL.getName()).getAsJsonArray())
+ .transform(JsonElement::getAsString)));
+ } else if (fields.contains(ChangeField.REVIEWER_BY_EMAIL.getName())) {
+ cd.setReviewersByEmail(ReviewerByEmailSet.empty());
+ }
+
decodeSubmitRecords(
source,
ChangeField.STORED_SUBMIT_RECORD_STRICT.getName(),
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
index 94b39a0..607df83 100644
--- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
+++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -35,9 +35,9 @@
import com.google.gson.JsonObject;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import io.searchbox.client.JestResult;
import io.searchbox.core.Bulk;
import io.searchbox.core.Bulk.Builder;
@@ -73,7 +73,7 @@
private final GroupMapping mapping;
private final Provider<GroupCache> groupCache;
- @AssistedInject
+ @Inject
ElasticGroupIndex(
@GerritServerConfig Config cfg,
SitePaths sitePaths,
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index 7375893..178b2bd 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-extension-api</artifactId>
- <version>2.14-SNAPSHOT</version>
+ <version>2.15-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Extension API</name>
<description>API for Gerrit Extensions</description>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
index 05fd5b2..1295ea0 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
@@ -21,5 +21,10 @@
return new ExportImpl(name);
}
+ /** Create an annotation to export based on a cannonical class name. */
+ public static Export named(Class<?> clazz) {
+ return named(clazz.getCanonicalName());
+ }
+
private Exports() {}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 27fdc18..1d4f0a8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -85,6 +85,8 @@
void move(MoveInput in) throws RestApiException;
+ void setPrivate(boolean value) throws RestApiException;
+
/**
* Create a new change that reverts this change.
*
@@ -307,6 +309,11 @@
}
@Override
+ public void setPrivate(boolean value) {
+ throw new NotImplementedException();
+ }
+
+ @Override
public ChangeApi revert() {
throw new NotImplementedException();
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java
index af61481..3a33de9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java
@@ -25,6 +25,13 @@
*/
@Nullable public Map<String, String> approvals;
+ public static ReviewerInfo byEmail(@Nullable String name, String email) {
+ ReviewerInfo info = new ReviewerInfo();
+ info.name = name;
+ info.email = email;
+ return info;
+ }
+
public ReviewerInfo(Integer id) {
super(id);
}
@@ -33,4 +40,6 @@
public String toString() {
return username;
}
+
+ private ReviewerInfo() {}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 5dd4ba4..9969995 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -18,6 +18,7 @@
import com.google.gerrit.extensions.common.ActionInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.extensions.common.MergeableInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
@@ -86,6 +87,17 @@
List<RobotCommentInfo> robotCommentsAsList() throws RestApiException;
+ /**
+ * Applies the indicated fix by creating a new change edit or integrating the fix with the
+ * existing change edit. If no change edit exists before this call, the fix must refer to the
+ * current patch set. If a change edit exists, the fix must refer to the patch set on which the
+ * change edit is based.
+ *
+ * @param fixId the ID of the fix which should be applied
+ * @throws RestApiException if the fix couldn't be applied
+ */
+ EditInfo applyFix(String fixId) throws RestApiException;
+
DraftApi createDraft(DraftInput in) throws RestApiException;
DraftApi draft(String id) throws RestApiException;
@@ -255,6 +267,11 @@
}
@Override
+ public EditInfo applyFix(String fixId) {
+ throw new NotImplementedException();
+ }
+
+ @Override
public Map<String, List<CommentInfo>> drafts() {
throw new NotImplementedException();
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommitApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommitApi.java
new file mode 100644
index 0000000..85bd952
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommitApi.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.projects;
+
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface CommitApi {
+
+ ChangeApi cherryPick(CherryPickInput input) throws RestApiException;
+
+ /** A default implementation for source compatibility when adding new methods to the interface. */
+ class NotImplemented implements CommitApi {
+ @Override
+ public ChangeApi cherryPick(CherryPickInput input) {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
index cc91a4a..e30a730 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -31,6 +31,7 @@
public InheritedBooleanInfo enableSignedPush;
public InheritedBooleanInfo requireSignedPush;
public InheritedBooleanInfo rejectImplicitMerges;
+ public InheritedBooleanInfo enableReviewerByEmail;
public MaxObjectSizeLimitInfo maxObjectSizeLimit;
public SubmitType submitType;
public ProjectState state;
@@ -40,6 +41,8 @@
public Map<String, CommentLinkInfo> commentlinks;
public ThemeInfo theme;
+ public Map<String, List<String>> extensionPanelNames;
+
public static class InheritedBooleanInfo {
public Boolean value;
public InheritableBoolean configuredValue;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
index ae81ea5..03b9772 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
@@ -29,6 +29,7 @@
public InheritableBoolean enableSignedPush;
public InheritableBoolean requireSignedPush;
public InheritableBoolean rejectImplicitMerges;
+ public InheritableBoolean enableReviewerByEmail;
public String maxObjectSizeLimit;
public SubmitType submitType;
public ProjectState state;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index dc2f899..6db13fc 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -125,6 +125,14 @@
TagApi tag(String ref) throws RestApiException;
/**
+ * Lookup a commit by its {@code ObjectId} string.
+ *
+ * @param commit the {@code ObjectId} string.
+ * @return API for accessing the commit.
+ */
+ CommitApi commit(String commit) throws RestApiException;
+
+ /**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
*/
@@ -218,5 +226,10 @@
public void deleteTags(DeleteTagsInput in) {
throw new NotImplementedException();
}
+
+ @Override
+ public CommitApi commit(String commit) {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
index 2225a99..3307997 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.client;
import java.sql.Timestamp;
+import java.util.Comparator;
import java.util.Objects;
public abstract class Comment {
@@ -36,7 +37,13 @@
public String message;
public Boolean unresolved;
- public static class Range {
+ public static class Range implements Comparable<Range> {
+ private static final Comparator<Range> RANGE_COMPARATOR =
+ Comparator.<Range>comparingInt(range -> range.startLine)
+ .thenComparingInt(range -> range.startCharacter)
+ .thenComparingInt(range -> range.endLine)
+ .thenComparingInt(range -> range.endCharacter);
+
public int startLine; // 1-based, inclusive
public int startCharacter; // 0-based, inclusive
public int endLine; // 1-based, exclusive
@@ -81,6 +88,11 @@
+ endCharacter
+ '}';
}
+
+ @Override
+ public int compareTo(Range otherRange) {
+ return RANGE_COMPARATOR.compare(this, otherRange);
+ }
}
public short side() {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
index 2fb32d7..f20509b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
@@ -15,6 +15,7 @@
package com.google.gerrit.extensions.common;
import java.util.List;
+import java.util.Objects;
public class AccountInfo {
public Integer _accountId;
@@ -29,4 +30,34 @@
public AccountInfo(Integer id) {
this._accountId = id;
}
+
+ /** To be used ONLY in connection with unregistered reviewers and CCs. */
+ public AccountInfo(String name, String email) {
+ this.name = name;
+ this.email = email;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof AccountInfo) {
+ AccountInfo accountInfo = (AccountInfo) o;
+ return Objects.equals(_accountId, accountInfo._accountId)
+ && Objects.equals(name, accountInfo.name)
+ && Objects.equals(email, accountInfo.email)
+ && Objects.equals(secondaryEmails, accountInfo.secondaryEmails)
+ && Objects.equals(username, accountInfo.username)
+ && Objects.equals(avatars, accountInfo.avatars)
+ && Objects.equals(_moreAccounts, accountInfo._moreAccounts)
+ && Objects.equals(status, accountInfo.status);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ _accountId, name, email, secondaryEmails, username, avatars, _moreAccounts, status);
+ }
+
+ protected AccountInfo() {}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 3803714..e13962d 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -44,6 +44,7 @@
public Integer insertions;
public Integer deletions;
public Integer unresolvedCommentCount;
+ public Boolean isPrivate;
public int _number;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
index e79918f..735b84f 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
@@ -20,6 +20,7 @@
public String id;
public String tag;
public AccountInfo author;
+ public AccountInfo realAuthor;
public Timestamp date;
public String message;
public Integer _revisionNumber;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java
index 2d1d840..13fc9ec 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java
@@ -19,4 +19,5 @@
public class PluginConfigInfo {
public Boolean hasAvatars;
public List<String> jsResourcePaths;
+ public List<String> htmlResourcePaths;
}
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index 8e503ee..b0429cb 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -15,7 +15,7 @@
package com.google.gerrit.gpg;
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableMap;
@@ -24,7 +24,7 @@
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.query.account.InternalAccountQuery;
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
index 62d0df7..c3dec61 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
@@ -17,8 +17,8 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import org.eclipse.jgit.lib.Repository;
@@ -30,7 +30,7 @@
private final GitRepositoryManager repoManager;
private final AllUsersName allUsers;
- @AssistedInject
+ @Inject
GerritPushCertificateChecker(
GerritPublicKeyChecker.Factory keyCheckerFactory,
GitRepositoryManager repoManager,
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
index 9aa18fe..14a4c6d 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
@@ -21,8 +21,8 @@
import com.google.gerrit.gpg.server.GpgKey;
import com.google.gerrit.gpg.server.GpgKeys;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import org.bouncycastle.openpgp.PGPException;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -36,7 +36,7 @@
private final DeleteGpgKey delete;
private final GpgKey rsrc;
- @AssistedInject
+ @Inject
GpgKeyApiImpl(GpgKeys.Get get, DeleteGpgKey delete, @Assisted GpgKey rsrc) {
this.get = get;
this.delete = delete;
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
index 50bf57b..64286c4 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -15,7 +15,7 @@
package com.google.gerrit.gpg.server;
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -26,8 +26,8 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.ExternalId;
-import com.google.gerrit.server.account.ExternalIdsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
index 819ad96..13fb368 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -14,12 +14,10 @@
package com.google.gerrit.gpg.server;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import static java.nio.charset.StandardCharsets.UTF_8;
-import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
-import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.extensions.common.GpgKeyInfo;
@@ -36,11 +34,11 @@
import com.google.gerrit.gpg.GerritPublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyStore;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResource;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -70,6 +68,7 @@
private final Provider<CurrentUser> self;
private final Provider<PublicKeyStore> storeProvider;
private final GerritPublicKeyChecker.Factory checkerFactory;
+ private final ExternalIds externalIds;
@Inject
GpgKeys(
@@ -77,12 +76,14 @@
Provider<ReviewDb> db,
Provider<CurrentUser> self,
Provider<PublicKeyStore> storeProvider,
- GerritPublicKeyChecker.Factory checkerFactory) {
+ GerritPublicKeyChecker.Factory checkerFactory,
+ ExternalIds externalIds) {
this.views = views;
this.db = db;
this.self = self;
this.storeProvider = storeProvider;
this.checkerFactory = checkerFactory;
+ this.externalIds = externalIds;
}
@Override
@@ -198,16 +199,8 @@
}
}
- @VisibleForTesting
- public static FluentIterable<ExternalId> getGpgExtIds(ReviewDb db, Account.Id accountId)
- throws OrmException {
- return FluentIterable.from(
- ExternalId.from(db.accountExternalIds().byAccount(accountId).toList()))
- .filter(in -> in.isScheme(SCHEME_GPGKEY));
- }
-
- private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws OrmException {
- return getGpgExtIds(db.get(), rsrc.getUser().getAccountId());
+ private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws IOException, OrmException {
+ return externalIds.byAccount(db.get(), rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
}
private static long keyId(byte[] fp) {
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 7b825b1..9c04ced 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -16,7 +16,7 @@
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
@@ -47,8 +47,9 @@
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.ExternalId;
-import com.google.gerrit.server.account.ExternalIdsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.mail.send.AddKeySender;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException;
@@ -91,6 +92,7 @@
private final AddKeySender.Factory addKeyFactory;
private final AccountCache accountCache;
private final Provider<InternalAccountQuery> accountQueryProvider;
+ private final ExternalIds externalIds;
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
@Inject
@@ -103,6 +105,7 @@
AddKeySender.Factory addKeyFactory,
AccountCache accountCache,
Provider<InternalAccountQuery> accountQueryProvider,
+ ExternalIds externalIds,
ExternalIdsUpdate.User externalIdsUpdateFactory) {
this.serverIdent = serverIdent;
this.db = db;
@@ -112,6 +115,7 @@
this.addKeyFactory = addKeyFactory;
this.accountCache = accountCache;
this.accountQueryProvider = accountQueryProvider;
+ this.externalIds = externalIds;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
}
@@ -122,7 +126,7 @@
GpgKeys.checkVisible(self, rsrc);
Collection<ExternalId> existingExtIds =
- GpgKeys.getGpgExtIds(db.get(), rsrc.getUser().getAccountId()).toList();
+ externalIds.byAccount(db.get(), rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
try (PublicKeyStore store = storeProvider.get()) {
Set<Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, toRemove);
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index 862930f..d82f95b 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -39,8 +39,8 @@
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.server.account.ExternalId;
-import com.google.gerrit.server.account.ExternalIdsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.schema.SchemaCreator;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
index 0de8b68..3cac62c 100644
--- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
+++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -138,6 +138,8 @@
public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
+ public final native boolean isPrivate() /*-{ return this.is_private ? true : false; }-*/;
+
public final native NativeMap<LabelInfo> allLabels() /*-{ return this.labels; }-*/;
public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index 9f87672..db8301a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -131,6 +131,7 @@
suggestions.add("is:open");
suggestions.add("is:pending");
suggestions.add("is:draft");
+ suggestions.add("is:private");
suggestions.add("is:closed");
suggestions.add("is:merged");
suggestions.add("is:abandoned");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
index 40116af..cb529f4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
@@ -52,8 +52,9 @@
userEmail.setText(account.email());
}
if (showSettingsLink) {
- if (Gerrit.info().auth().switchAccountUrl() != null) {
- switchAccount.setHref(Gerrit.info().auth().switchAccountUrl());
+ String switchAccountUrl = Gerrit.info().auth().switchAccountUrl();
+ if (switchAccountUrl != null) {
+ switchAccount.setHref(switchAccountUrl.replace("${path}", "/"));
} else if (Gerrit.info().auth().isDev() || Gerrit.info().auth().isOpenId()) {
switchAccount.setHref(Gerrit.selfRedirect("/login"));
} else {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index d7fb072..1aecd08 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -75,6 +75,8 @@
String rejectImplicitMerges();
+ String enableReviewerByEmail();
+
String headingMaxObjectSizeLimit();
String headingGroupOptions();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 465bcfc..4c7153e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -37,6 +37,7 @@
headingParentProjectName = Rights Inherit From
parentSuggestions = Parent Suggestion
columnProjectName = Project Name
+enableReviewerByEmail = Enable adding unregistered users as reviewers and CCs on changes
headingGroupUUID = Group UUID
headingOwner = Owners
@@ -148,7 +149,8 @@
removeReviewer, \
submit, \
submitAs, \
- viewDrafts
+ viewDrafts, \
+ viewPrivateChanges
abandon = Abandon
addPatchSet = Add Patch Set
@@ -174,6 +176,7 @@
submit = Submit
submitAs = Submit (On Behalf Of)
viewDrafts = View Drafts
+viewPrivateChanges = View Private Changes
refErrorEmpty = Reference must be supplied
refErrorBeginSlash = Reference must not start with '/'
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index 3645fb9..2f5caf8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -86,6 +86,7 @@
private ListBox enableSignedPush;
private ListBox requireSignedPush;
private ListBox rejectImplicitMerges;
+ private ListBox enableReviewerByEmail;
private NpTextBox maxObjectSizeLimit;
private Label effectiveMaxObjectSizeLimit;
private Map<String, Map<String, HasEnabled>> pluginConfigWidgets;
@@ -191,6 +192,7 @@
requireChangeID.setEnabled(isOwner);
rejectImplicitMerges.setEnabled(isOwner);
maxObjectSizeLimit.setEnabled(isOwner);
+ enableReviewerByEmail.setEnabled(isOwner);
if (pluginConfigWidgets != null) {
for (Map<String, HasEnabled> widgetMap : pluginConfigWidgets.values()) {
@@ -264,6 +266,10 @@
saveEnabler.listenTo(rejectImplicitMerges);
grid.addHtml(AdminConstants.I.rejectImplicitMerges(), rejectImplicitMerges);
+ enableReviewerByEmail = newInheritedBooleanBox();
+ saveEnabler.listenTo(enableReviewerByEmail);
+ grid.addHtml(AdminConstants.I.enableReviewerByEmail(), enableReviewerByEmail);
+
maxObjectSizeLimit = new NpTextBox();
saveEnabler.listenTo(maxObjectSizeLimit);
effectiveMaxObjectSizeLimit = new Label();
@@ -395,6 +401,7 @@
setBool(requireSignedPush, result.requireSignedPush());
}
setBool(rejectImplicitMerges, result.rejectImplicitMerges());
+ setBool(enableReviewerByEmail, result.enableReviewerByEmail());
setSubmitType(result.submitType());
setState(result.state());
maxObjectSizeLimit.setText(result.maxObjectSizeLimit().configuredValue());
@@ -665,6 +672,7 @@
esp,
rsp,
getBool(rejectImplicitMerges),
+ getBool(enableReviewerByEmail),
maxObjectSizeLimit.getText().trim(),
SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
ProjectState.valueOf(state.getValue(state.getSelectedIndex())),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
index 1555f56..294fa9b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
@@ -100,9 +100,9 @@
var s = new SettingsScreenDefinition(p,m,c);
(this.settingsScreens[n] || (this.settingsScreens[n]=[])).push(s);
},
- panel: function(i,c){this._panel(this.getPluginName(),i,c)},
- _panel: function(n,i,c){
- var p = new PanelDefinition(n,c);
+ panel: function(i,c,n){this._panel(this.getPluginName(),i,c,n)},
+ _panel: function(n,i,c,x){
+ var p = new PanelDefinition(n,c,x);
(this.panels[i] || (this.panels[i]=[])).push(p);
},
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
index 0873363..6d3dd60 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
@@ -22,7 +22,10 @@
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -32,13 +35,17 @@
private final List<Context> contexts;
public ExtensionPanel(GerritUiExtensionPoint extensionPoint) {
- this.extensionPoint = extensionPoint;
- this.contexts = create();
+ this(extensionPoint, new ArrayList<String>());
}
- private List<Context> create() {
+ public ExtensionPanel(GerritUiExtensionPoint extensionPoint, List<String> panelNames) {
+ this.extensionPoint = extensionPoint;
+ this.contexts = create(panelNames);
+ }
+
+ private List<Context> create(List<String> panelNames) {
List<Context> contexts = new ArrayList<>();
- for (Definition def : Natives.asList(Definition.get(extensionPoint.name()))) {
+ for (Definition def : getOrderedDefs(panelNames)) {
SimplePanel p = new SimplePanel();
add(p);
contexts.add(Context.create(def, p));
@@ -46,6 +53,42 @@
return contexts;
}
+ private List<Definition> getOrderedDefs(List<String> panelNames) {
+ if (panelNames == null) {
+ panelNames = Collections.emptyList();
+ }
+ Map<String, List<Definition>> defsOrderedByName = new LinkedHashMap<>();
+ for (String name : panelNames) {
+ defsOrderedByName.put(name, new ArrayList<Definition>());
+ }
+ for (Definition def : Natives.asList(Definition.get(extensionPoint.name()))) {
+ addDef(def, defsOrderedByName);
+ }
+ List<Definition> orderedDefs = new ArrayList<>();
+ for (List<Definition> defList : defsOrderedByName.values()) {
+ orderedDefs.addAll(defList);
+ }
+ return orderedDefs;
+ }
+
+ private static void addDef(Definition def, Map<String, List<Definition>> defsOrderedByName) {
+ String panelName = def.getPanelName();
+ if (panelName.equals(def.getPluginName() + ".undefined")) {
+ /* Handle a partially undefined panel name from the
+ javascript layer by generating a random panel name.
+ This maintains support for panels that do not provide a name. */
+ panelName =
+ def.getPluginName() + "." + Long.toHexString(Double.doubleToLongBits(Math.random()));
+ }
+ if (defsOrderedByName.containsKey(panelName)) {
+ defsOrderedByName.get(panelName).add(def);
+ } else if (defsOrderedByName.containsKey(def.getPluginName())) {
+ defsOrderedByName.get(def.getPluginName()).add(def);
+ } else {
+ defsOrderedByName.put(panelName, Collections.singletonList(def));
+ }
+ }
+
public void put(GerritUiExtensionPoint.Key key, String value) {
for (Context ctx : contexts) {
ctx.put(key.name(), value);
@@ -103,9 +146,10 @@
static final JavaScriptObject TYPE = init();
private static native JavaScriptObject init() /*-{
- function PanelDefinition(n, c) {
+ function PanelDefinition(n, c, x) {
this.pluginName = n;
this.onLoad = c;
+ this.name = x;
};
return PanelDefinition;
}-*/;
@@ -113,6 +157,10 @@
static native JsArray<Definition> get(String i) /*-{ return $wnd.Gerrit.panels[i] || [] }-*/;
protected Definition() {}
+
+ public final native String getPanelName() /*-{ return this.pluginName + "." + this.name; }-*/;
+
+ public final native String getPluginName() /*-{ return this.pluginName; }-*/;
}
static class Context extends JavaScriptObject {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
index 29787b8..48a812c1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
@@ -68,7 +68,7 @@
onAction: function(t,n,c){G._onAction(this.name,t,n,c)},
screen: function(p,c){G._screen(this.name,p,c)},
settingsScreen: function(p,m,c){G._settingsScreen(this.name,p,m,c)},
- panel: function(i,c){G._panel(this.name,i,c)},
+ panel: function(i,c,n){G._panel(this.name,i,c,n)},
url: function (u){return G.url(this._url(u))},
get: function(u,b){@com.google.gerrit.client.api.ActionContext::get(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
index 1c59dac..8a479dd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
@@ -28,6 +28,7 @@
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwtexpui.progress.client.ProgressBar;
import java.util.List;
+import java.util.stream.Collectors;
/** Loads JavaScript plugins with a progress meter visible. */
public class PluginLoader extends DialogBox {
@@ -38,10 +39,15 @@
if (plugins == null || plugins.isEmpty()) {
callback.onSuccess(VoidResult.create());
} else {
- self = new PluginLoader(loadTimeout, callback);
- self.load(plugins);
- self.startTimers();
- self.center();
+ plugins = plugins.stream().filter(p -> p.endsWith(".js")).collect(Collectors.toList());
+ if (plugins.isEmpty()) {
+ callback.onSuccess(VoidResult.create());
+ } else {
+ self = new PluginLoader(loadTimeout, callback);
+ self.load(plugins);
+ self.startTimers();
+ self.center();
+ }
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
index ada28af..b22b79f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -48,6 +48,7 @@
"revert",
"submit",
"topic",
+ "private",
"/",
};
@@ -65,6 +66,9 @@
@UiField Button deleteChange;
+ @UiField Button markPrivate;
+ @UiField Button unmarkPrivate;
+
@UiField Button restore;
private RestoreAction restoreAction;
@@ -122,6 +126,11 @@
a2b(actions, "restore", restore);
a2b(actions, "revert", revert);
a2b(actions, "followup", followUp);
+ if (info.isPrivate()) {
+ a2b(actions, "private", unmarkPrivate);
+ } else {
+ a2b(actions, "private", markPrivate);
+ }
for (String id : filterNonCore(actions)) {
add(new ActionButton(info, actions.get(id)));
}
@@ -192,6 +201,16 @@
}
}
+ @UiHandler("markPrivate")
+ void onMarkPrivate(@SuppressWarnings("unused") ClickEvent e) {
+ ChangeActions.markPrivate(changeId, markPrivate);
+ }
+
+ @UiHandler("unmarkPrivate")
+ void onUnmarkPrivate(@SuppressWarnings("unused") ClickEvent e) {
+ ChangeActions.unmarkPrivate(changeId, unmarkPrivate);
+ }
+
@UiHandler("restore")
void onRestore(@SuppressWarnings("unused") ClickEvent e) {
if (restoreAction == null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
index d0e5c3e..60efc8c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
@@ -81,6 +81,12 @@
<g:Button ui:field='followUp' styleName='' visible='false'>
<div><ui:msg>Follow-Up</ui:msg></div>
</g:Button>
+ <g:Button ui:field='markPrivate' styleName='' visible='false'>
+ <div><ui:msg>Mark Private</ui:msg></div>
+ </g:Button>
+ <g:Button ui:field='unmarkPrivate' styleName='' visible='false'>
+ <div><ui:msg>Unmark Private</ui:msg></div>
+ </g:Button>
<g:Button ui:field='submit' styleName='{style.submit}' visible='false'/>
</g:FlowPanel>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeActions.java
index 1be60cc..b8fcab7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeActions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeActions.java
@@ -37,6 +37,14 @@
ChangeApi.deleteChange(id.get(), mine(draftButtons));
}
+ static void markPrivate(Change.Id id, Button... draftButtons) {
+ ChangeApi.markPrivate(id.get(), cs(id, draftButtons));
+ }
+
+ static void unmarkPrivate(Change.Id id, Button... draftButtons) {
+ ChangeApi.unmarkPrivate(id.get(), cs(id, draftButtons));
+ }
+
public static GerritCallback<JavaScriptObject> cs(
final Change.Id id, final Button... draftButtons) {
setEnabled(false, draftButtons);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
index e91b6f3..0c176e8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
@@ -198,6 +198,7 @@
@UiField InlineLabel uploaderName;
@UiField Element statusText;
+ @UiField Element privateText;
@UiField Image projectSettings;
@UiField AnchorElement projectSettingsLink;
@UiField InlineHyperlink projectDashboard;
@@ -308,8 +309,7 @@
@Override
public void onSuccess(final ChangeInfo info) {
info.init();
- addExtensionPoints(info, initCurrentRevision(info));
-
+ initCurrentRevision(info);
final RevisionInfo rev = info.revision(revision);
CallbackGroup group = new CallbackGroup();
loadCommit(rev, group);
@@ -378,7 +378,7 @@
return resolveRevisionToDisplay(info);
}
- private void addExtensionPoints(ChangeInfo change, RevisionInfo rev) {
+ private void addExtensionPoints(ChangeInfo change, RevisionInfo rev, Entry result) {
addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER, headerExtension, change, rev);
addExtensionPoint(
GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS,
@@ -391,7 +391,12 @@
change,
rev);
addExtensionPoint(
- GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK, changeExtension, change, rev);
+ GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK,
+ changeExtension,
+ change,
+ rev,
+ result.getExtensionPanelNames(
+ GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK.toString()));
addExtensionPoint(
GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_RELATED_INFO_BLOCK,
relatedExtension,
@@ -407,13 +412,22 @@
}
private void addExtensionPoint(
- GerritUiExtensionPoint extensionPoint, Panel p, ChangeInfo change, RevisionInfo rev) {
- ExtensionPanel extensionPanel = new ExtensionPanel(extensionPoint);
+ GerritUiExtensionPoint extensionPoint,
+ Panel p,
+ ChangeInfo change,
+ RevisionInfo rev,
+ List<String> panelNames) {
+ ExtensionPanel extensionPanel = new ExtensionPanel(extensionPoint, panelNames);
extensionPanel.putObject(GerritUiExtensionPoint.Key.CHANGE_INFO, change);
extensionPanel.putObject(GerritUiExtensionPoint.Key.REVISION_INFO, rev);
p.add(extensionPanel);
}
+ private void addExtensionPoint(
+ GerritUiExtensionPoint extensionPoint, Panel p, ChangeInfo change, RevisionInfo rev) {
+ addExtensionPoint(extensionPoint, p, change, rev, Collections.emptyList());
+ }
+
private boolean enableSignedPush() {
return Gerrit.info().receive().enableSignedPush();
}
@@ -1030,6 +1044,14 @@
loadRevisionInfo();
}
});
+ ConfigInfoCache.get(
+ info.projectNameKey(),
+ new GerritCallback<Entry>() {
+ @Override
+ public void onSuccess(Entry entry) {
+ addExtensionPoints(info, rev, entry);
+ }
+ });
}
private void updateToken(ChangeInfo info, DiffObject base, RevisionInfo rev) {
@@ -1366,6 +1388,10 @@
statusText.setInnerText(Util.toLongString(s));
}
+ if (info.isPrivate()) {
+ privateText.setInnerText(Util.C.isPrivate());
+ }
+
if (Gerrit.isSignedIn()) {
replyAction =
new ReplyAction(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
index 152b157..e2297cb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
@@ -99,6 +99,9 @@
.statusText {
font-weight: bold;
}
+ .privateText {
+ font-weight: bold;
+ }
div.popdown {
display: inline-block;
@@ -376,7 +379,8 @@
<span class='{style.changeId}'>
<ui:msg>Change <g:Anchor ui:field='permalink' title='Reload the change (Shortcut: R)'>
<ui:attribute name='title'/>
- </g:Anchor> - <span ui:field='statusText' class='{style.statusText}'/></ui:msg>
+ </g:Anchor> - <span ui:field='statusText' class='{style.statusText}'/>
+ <span ui:field='privateText' class='{style.privateText}'/></ui:msg>
</span>
<g:SimplePanel ui:field='headerExtension' styleName='{style.headerExtension}'/>
</div>
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 f8a9ba1..f985f31 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
@@ -121,6 +121,14 @@
change(id).view("assignee").put(input, cb);
}
+ public static void markPrivate(int id, AsyncCallback<JavaScriptObject> cb) {
+ change(id).view("private").put(cb);
+ }
+
+ public static void unmarkPrivate(int id, AsyncCallback<JavaScriptObject> cb) {
+ change(id).view("private").delete(cb);
+ }
+
public static RestApi comments(int id) {
return call(id, "comments");
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index ae64ac0..4543217 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -33,6 +33,8 @@
String notCurrent();
+ String isPrivate();
+
String changeEdit();
String myDashboardTitle();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 01921de..3545a2f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -7,6 +7,7 @@
mergeConflict = Merge Conflict
notCurrent = Not Current
changeEdit = Change Edit
+isPrivate = (Private)
myDashboardTitle = My Reviews
unknownDashboardTitle = Code Review Dashboard
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index adf7cff..055044c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -237,9 +237,17 @@
Change.Status status = c.status();
if (status != Change.Status.NEW) {
- table.setText(row, C_STATUS, Util.toLongString(status));
+ table.setText(
+ row,
+ C_STATUS,
+ Util.toLongString(status) + (c.isPrivate() ? (" " + Util.C.isPrivate()) : ""));
} else if (!c.mergeable()) {
- table.setText(row, C_STATUS, Util.C.changeTableNotMergeable());
+ table.setText(
+ row,
+ C_STATUS,
+ Util.C.changeTableNotMergeable() + (c.isPrivate() ? (" " + Util.C.isPrivate()) : ""));
+ } else if (c.isPrivate()) {
+ table.setText(row, C_STATUS, Util.C.isPrivate());
}
if (c.owner() != null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
index 953bc87..0091f53 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
@@ -68,23 +68,15 @@
colorLines(cm, LineClassWhere.WRAP, color, line, line + cnt);
}
- void colorLines(
- final CodeMirror cm,
- final LineClassWhere where,
- final String className,
- final int start,
- final int end) {
+ void colorLines(CodeMirror cm, LineClassWhere where, String className, int start, int end) {
if (start < end) {
for (int line = start; line < end; line++) {
cm.addLineClass(line, where, className);
}
undo.add(
- new Runnable() {
- @Override
- public void run() {
- for (int line = start; line < end; line++) {
- cm.removeLineClass(line, where, className);
- }
+ () -> {
+ for (int line = start; line < end; line++) {
+ cm.removeLineClass(line, where, className);
}
});
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
index 587dacc..95b88ac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
@@ -203,32 +203,26 @@
abstract String getTokenSuffixForActiveLine(CodeMirror cm);
- Runnable signInCallback(final CodeMirror cm) {
- return new Runnable() {
- @Override
- public void run() {
- String token = host.getToken();
- if (cm.extras().hasActiveLine()) {
- token += "@" + getTokenSuffixForActiveLine(cm);
- }
- Gerrit.doSignIn(token);
+ Runnable signInCallback(CodeMirror cm) {
+ return () -> {
+ String token = host.getToken();
+ if (cm.extras().hasActiveLine()) {
+ token += "@" + getTokenSuffixForActiveLine(cm);
}
+ Gerrit.doSignIn(token);
};
}
abstract void newDraft(CodeMirror cm);
- Runnable newDraftCallback(final CodeMirror cm) {
+ Runnable newDraftCallback(CodeMirror cm) {
if (!Gerrit.isSignedIn()) {
return signInCallback(cm);
}
- return new Runnable() {
- @Override
- public void run() {
- if (cm.extras().hasActiveLine()) {
- newDraft(cm);
- }
+ return () -> {
+ if (cm.extras().hasActiveLine()) {
+ newDraft(cm);
}
};
}
@@ -267,52 +261,49 @@
abstract SortedMap<Integer, CommentGroup> getMapForNav(DisplaySide side);
- Runnable commentNav(final CodeMirror src, final Direction dir) {
- return new Runnable() {
- @Override
- public void run() {
- // Every comment appears in both side maps as a linked pair.
- // It is only necessary to search one side to find a comment
- // on either side of the editor pair.
- SortedMap<Integer, CommentGroup> map = getMapForNav(src.side());
- int line =
- src.extras().hasActiveLine() ? src.getLineNumber(src.extras().activeLine()) + 1 : 0;
+ Runnable commentNav(CodeMirror src, Direction dir) {
+ return () -> {
+ // Every comment appears in both side maps as a linked pair.
+ // It is only necessary to search one side to find a comment
+ // on either side of the editor pair.
+ SortedMap<Integer, CommentGroup> map = getMapForNav(src.side());
+ int line =
+ src.extras().hasActiveLine() ? src.getLineNumber(src.extras().activeLine()) + 1 : 0;
- CommentGroup g;
- if (dir == Direction.NEXT) {
- map = map.tailMap(line + 1);
+ CommentGroup g;
+ if (dir == Direction.NEXT) {
+ map = map.tailMap(line + 1);
+ if (map.isEmpty()) {
+ return;
+ }
+ g = map.get(map.firstKey());
+ while (g.getBoxCount() == 0) {
+ map = map.tailMap(map.firstKey() + 1);
if (map.isEmpty()) {
return;
}
g = map.get(map.firstKey());
- while (g.getBoxCount() == 0) {
- map = map.tailMap(map.firstKey() + 1);
- if (map.isEmpty()) {
- return;
- }
- g = map.get(map.firstKey());
- }
- } else {
- map = map.headMap(line);
+ }
+ } else {
+ map = map.headMap(line);
+ if (map.isEmpty()) {
+ return;
+ }
+ g = map.get(map.lastKey());
+ while (g.getBoxCount() == 0) {
+ map = map.headMap(map.lastKey());
if (map.isEmpty()) {
return;
}
g = map.get(map.lastKey());
- while (g.getBoxCount() == 0) {
- map = map.headMap(map.lastKey());
- if (map.isEmpty()) {
- return;
- }
- g = map.get(map.lastKey());
- }
}
-
- CodeMirror cm = g.getCm();
- double y = cm.heightAtLine(g.getLine() - 1, "local");
- cm.setCursor(Pos.create(g.getLine() - 1));
- cm.scrollToY(y - 0.5 * cm.scrollbarV().getClientHeight());
- cm.focus();
}
+
+ CodeMirror cm = g.getCm();
+ double y = cm.heightAtLine(g.getLine() - 1, "local");
+ cm.setCursor(Pos.create(g.getLine() - 1));
+ cm.scrollToY(y - 0.5 * cm.scrollbarV().getClientHeight());
+ cm.focus();
};
}
@@ -425,26 +416,20 @@
abstract CommentGroup getCommentGroupOnActiveLine(CodeMirror cm);
- Runnable toggleOpenBox(final CodeMirror cm) {
- return new Runnable() {
- @Override
- public void run() {
- CommentGroup group = getCommentGroupOnActiveLine(cm);
- if (group != null) {
- group.openCloseLast();
- }
+ Runnable toggleOpenBox(CodeMirror cm) {
+ return () -> {
+ CommentGroup group = getCommentGroupOnActiveLine(cm);
+ if (group != null) {
+ group.openCloseLast();
}
};
}
- Runnable openCloseAll(final CodeMirror cm) {
- return new Runnable() {
- @Override
- public void run() {
- CommentGroup group = getCommentGroupOnActiveLine(cm);
- if (group != null) {
- group.openCloseAll();
- }
+ Runnable openCloseAll(CodeMirror cm) {
+ return () -> {
+ CommentGroup group = getCommentGroupOnActiveLine(cm);
+ if (group != null) {
+ group.openCloseAll();
}
};
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
index 60a75eb..702383a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
@@ -336,7 +336,7 @@
handlers.clear();
}
- void registerCmEvents(final CodeMirror cm) {
+ void registerCmEvents(CodeMirror cm) {
cm.on("cursorActivity", updateActiveLine(cm));
cm.on("focus", updateActiveLine(cm));
KeyMap keyMap =
@@ -356,170 +356,45 @@
.on("Shift-O", getCommentManager().openCloseAll(cm))
.on(
"I",
- new Runnable() {
- @Override
- public void run() {
- switch (getIntraLineStatus()) {
- case OFF:
- case OK:
- toggleShowIntraline();
- break;
- case FAILURE:
- case TIMEOUT:
- default:
- break;
- }
+ () -> {
+ switch (getIntraLineStatus()) {
+ case OFF:
+ case OK:
+ toggleShowIntraline();
+ break;
+ case FAILURE:
+ case TIMEOUT:
+ default:
+ break;
}
})
- .on(
- "','",
- new Runnable() {
- @Override
- public void run() {
- prefsAction.show();
- }
- })
- .on(
- "Shift-/",
- new Runnable() {
- @Override
- public void run() {
- new ShowHelpCommand().onKeyPress(null);
- }
- })
- .on(
- "Space",
- new Runnable() {
- @Override
- public void run() {
- cm.vim().handleKey("<C-d>");
- }
- })
- .on(
- "Shift-Space",
- new Runnable() {
- @Override
- public void run() {
- cm.vim().handleKey("<C-u>");
- }
- })
- .on(
- "Ctrl-F",
- new Runnable() {
- @Override
- public void run() {
- cm.execCommand("find");
- }
- })
- .on(
- "Ctrl-G",
- new Runnable() {
- @Override
- public void run() {
- cm.execCommand("findNext");
- }
- })
+ .on("','", prefsAction::show)
+ .on("Shift-/", () -> new ShowHelpCommand().onKeyPress(null))
+ .on("Space", () -> cm.vim().handleKey("<C-d>"))
+ .on("Shift-Space", () -> cm.vim().handleKey("<C-u>"))
+ .on("Ctrl-F", () -> cm.execCommand("find"))
+ .on("Ctrl-G", () -> cm.execCommand("findNext"))
.on("Enter", maybeNextCmSearch(cm))
- .on(
- "Shift-Ctrl-G",
- new Runnable() {
- @Override
- public void run() {
- cm.execCommand("findPrev");
- }
- })
- .on(
- "Shift-Enter",
- new Runnable() {
- @Override
- public void run() {
- cm.execCommand("findPrev");
- }
- })
+ .on("Shift-Ctrl-G", () -> cm.execCommand("findPrev"))
+ .on("Shift-Enter", () -> cm.execCommand("findPrev"))
.on(
"Esc",
- new Runnable() {
- @Override
- public void run() {
- cm.setCursor(cm.getCursor());
- cm.execCommand("clearSearch");
- cm.vim().handleEx("nohlsearch");
- }
+ () -> {
+ cm.setCursor(cm.getCursor());
+ cm.execCommand("clearSearch");
+ cm.vim().handleEx("nohlsearch");
})
- .on(
- "Ctrl-A",
- new Runnable() {
- @Override
- public void run() {
- cm.execCommand("selectAll");
- }
- })
- .on(
- "G O",
- new Runnable() {
- @Override
- public void run() {
- Gerrit.display(PageLinks.toChangeQuery("status:open"));
- }
- })
- .on(
- "G M",
- new Runnable() {
- @Override
- public void run() {
- Gerrit.display(PageLinks.toChangeQuery("status:merged"));
- }
- })
- .on(
- "G A",
- new Runnable() {
- @Override
- public void run() {
- Gerrit.display(PageLinks.toChangeQuery("status:abandoned"));
- }
- });
+ .on("Ctrl-A", () -> cm.execCommand("selectAll"))
+ .on("G O", () -> Gerrit.display(PageLinks.toChangeQuery("status:open")))
+ .on("G M", () -> Gerrit.display(PageLinks.toChangeQuery("status:merged")))
+ .on("G A", () -> Gerrit.display(PageLinks.toChangeQuery("status:abandoned")));
if (Gerrit.isSignedIn()) {
keyMap
- .on(
- "G I",
- new Runnable() {
- @Override
- public void run() {
- Gerrit.display(PageLinks.MINE);
- }
- })
- .on(
- "G D",
- new Runnable() {
- @Override
- public void run() {
- Gerrit.display(PageLinks.toChangeQuery("owner:self is:draft"));
- }
- })
- .on(
- "G C",
- new Runnable() {
- @Override
- public void run() {
- Gerrit.display(PageLinks.toChangeQuery("has:draft"));
- }
- })
- .on(
- "G W",
- new Runnable() {
- @Override
- public void run() {
- Gerrit.display(PageLinks.toChangeQuery("is:watched status:open"));
- }
- })
- .on(
- "G S",
- new Runnable() {
- @Override
- public void run() {
- Gerrit.display(PageLinks.toChangeQuery("is:starred"));
- }
- });
+ .on("G I", () -> Gerrit.display(PageLinks.MINE))
+ .on("G D", () -> Gerrit.display(PageLinks.toChangeQuery("owner:self is:draft")))
+ .on("G C", () -> Gerrit.display(PageLinks.toChangeQuery("has:draft")))
+ .on("G W", () -> Gerrit.display(PageLinks.toChangeQuery("is:watched status:open")))
+ .on("G S", () -> Gerrit.display(PageLinks.toChangeQuery("is:starred")));
}
if (revision.get() != 0) {
@@ -698,15 +573,12 @@
abstract void setSyntaxHighlighting(boolean b);
- void setContext(final int context) {
+ void setContext(int context) {
operation(
- new Runnable() {
- @Override
- public void run() {
- skipManager.removeAll();
- skipManager.render(context, diff);
- updateRenderEntireFile();
- }
+ () -> {
+ skipManager.removeAll();
+ skipManager.render(context, diff);
+ updateRenderEntireFile();
});
}
@@ -753,21 +625,18 @@
return line - offset;
}
- private Runnable openEditScreen(final CodeMirror cm) {
- return new Runnable() {
- @Override
- public void run() {
- LineHandle handle = cm.extras().activeLine();
- int line = cm.getLineNumber(handle) + 1;
- if (Patch.COMMIT_MSG.equals(path)) {
- line = adjustCommitMessageLine(line);
- }
- String token = Dispatcher.toEditScreen(revision, path, line);
- if (!Gerrit.isSignedIn()) {
- Gerrit.doSignIn(token);
- } else {
- Gerrit.display(token);
- }
+ private Runnable openEditScreen(CodeMirror cm) {
+ return () -> {
+ LineHandle handle = cm.extras().activeLine();
+ int line = cm.getLineNumber(handle) + 1;
+ if (Patch.COMMIT_MSG.equals(path)) {
+ line = adjustCommitMessageLine(line);
+ }
+ String token = Dispatcher.toEditScreen(revision, path, line);
+ if (!Gerrit.isSignedIn()) {
+ Gerrit.doSignIn(token);
+ } else {
+ Gerrit.display(token);
}
};
}
@@ -832,63 +701,51 @@
abstract void operation(Runnable apply);
- private Runnable upToChange(final boolean openReplyBox) {
- return new Runnable() {
- @Override
- public void run() {
- CallbackGroup group = new CallbackGroup();
- getCommentManager().saveAllDrafts(group);
- group.done();
- group.addListener(
- new GerritCallback<Void>() {
- @Override
- public void onSuccess(Void result) {
- String rev = String.valueOf(revision.get());
- Gerrit.display(
- PageLinks.toChange(changeId, base.asString(), rev),
- new ChangeScreen(changeId, base, rev, openReplyBox, FileTable.Mode.REVIEW));
- }
- });
+ private Runnable upToChange(boolean openReplyBox) {
+ return () -> {
+ CallbackGroup group = new CallbackGroup();
+ getCommentManager().saveAllDrafts(group);
+ group.done();
+ group.addListener(
+ new GerritCallback<Void>() {
+ @Override
+ public void onSuccess(Void result) {
+ String rev = String.valueOf(revision.get());
+ Gerrit.display(
+ PageLinks.toChange(changeId, base.asString(), rev),
+ new ChangeScreen(changeId, base, rev, openReplyBox, FileTable.Mode.REVIEW));
+ }
+ });
+ };
+ }
+
+ private Runnable maybePrevVimSearch(CodeMirror cm) {
+ return () -> {
+ if (cm.vim().hasSearchHighlight()) {
+ cm.vim().handleKey("N");
+ } else {
+ getCommentManager().commentNav(cm, Direction.NEXT).run();
}
};
}
- private Runnable maybePrevVimSearch(final CodeMirror cm) {
- return new Runnable() {
- @Override
- public void run() {
- if (cm.vim().hasSearchHighlight()) {
- cm.vim().handleKey("N");
- } else {
- getCommentManager().commentNav(cm, Direction.NEXT).run();
- }
+ private Runnable maybeNextVimSearch(CodeMirror cm) {
+ return () -> {
+ if (cm.vim().hasSearchHighlight()) {
+ cm.vim().handleKey("n");
+ } else {
+ getChunkManager().diffChunkNav(cm, Direction.NEXT).run();
}
};
}
- private Runnable maybeNextVimSearch(final CodeMirror cm) {
- return new Runnable() {
- @Override
- public void run() {
- if (cm.vim().hasSearchHighlight()) {
- cm.vim().handleKey("n");
- } else {
- getChunkManager().diffChunkNav(cm, Direction.NEXT).run();
- }
- }
- };
- }
-
- Runnable maybeNextCmSearch(final CodeMirror cm) {
- return new Runnable() {
- @Override
- public void run() {
- if (cm.hasSearchHighlight()) {
- cm.execCommand("findNext");
- } else {
- cm.execCommand("clearSearch");
- getCommentManager().toggleOpenBox(cm).run();
- }
+ Runnable maybeNextCmSearch(CodeMirror cm) {
+ return () -> {
+ if (cm.hasSearchHighlight()) {
+ cm.execCommand("findNext");
+ } else {
+ cm.execCommand("clearSearch");
+ getCommentManager().toggleOpenBox(cm).run();
}
};
}
@@ -973,7 +830,7 @@
}
void reloadDiffInfo() {
- final int id = ++reloadVersionId;
+ int id = ++reloadVersionId;
DiffApi.diff(revision, path)
.base(base.asPatchSetId())
.wholeFile()
@@ -986,16 +843,13 @@
if (id == reloadVersionId && isAttached()) {
diff = diffInfo;
operation(
- new Runnable() {
- @Override
- public void run() {
- skipManager.removeAll();
- getChunkManager().reset();
- getDiffTable().scrollbar.removeDiffAnnotations();
- setShowIntraline(prefs.intralineDifference());
- render(diff);
- skipManager.render(prefs.context(), diff);
- }
+ () -> {
+ skipManager.removeAll();
+ getChunkManager().reset();
+ getDiffTable().scrollbar.removeDiffAnnotations();
+ setShowIntraline(prefs.intralineDifference());
+ render(diff);
+ skipManager.render(prefs.context(), diff);
});
}
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
index a2ffb03f..bf9f9e3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
@@ -318,47 +318,26 @@
}
Runnable toggleReviewed() {
- return new Runnable() {
- @Override
- public void run() {
- reviewed.setValue(!reviewed.getValue(), true);
- }
- };
+ return () -> reviewed.setValue(!reviewed.getValue(), true);
}
Runnable navigate(Direction dir) {
switch (dir) {
case PREV:
- return new Runnable() {
- @Override
- public void run() {
- (hasPrev ? prev : up).go();
- }
- };
+ return () -> (hasPrev ? prev : up).go();
case NEXT:
- return new Runnable() {
- @Override
- public void run() {
- (hasNext ? next : up).go();
- }
- };
+ return () -> (hasNext ? next : up).go();
default:
- return new Runnable() {
- @Override
- public void run() {}
- };
+ return () -> {};
}
}
Runnable reviewedAndNext() {
- return new Runnable() {
- @Override
- public void run() {
- if (Gerrit.isSignedIn()) {
- reviewed.setValue(true, true);
- }
- navigate(Direction.NEXT).run();
+ return () -> {
+ if (Gerrit.isSignedIn()) {
+ reviewed.setValue(true, true);
}
+ navigate(Direction.NEXT).run();
};
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
index 4d781ea..ed4ac25 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
@@ -322,13 +322,10 @@
prefs.tabSize(Math.max(1, Integer.parseInt(v)));
if (view != null) {
view.operation(
- new Runnable() {
- @Override
- public void run() {
- int v = prefs.tabSize();
- for (CodeMirror cm : view.getCms()) {
- cm.setOption("tabSize", v);
- }
+ () -> {
+ int size = prefs.tabSize();
+ for (CodeMirror cm : view.getCms()) {
+ cm.setOption("tabSize", size);
}
});
}
@@ -341,13 +338,7 @@
if (v != null && v.length() > 0) {
prefs.lineLength(Math.max(1, Integer.parseInt(v)));
if (view != null) {
- view.operation(
- new Runnable() {
- @Override
- public void run() {
- view.setLineLength(prefs.lineLength());
- }
- });
+ view.operation(() -> view.setLineLength(prefs.lineLength()));
}
}
}
@@ -448,7 +439,7 @@
@UiHandler("mode")
void onMode(@SuppressWarnings("unused") ChangeEvent e) {
- final String mode = getSelectedMode();
+ String mode = getSelectedMode();
prefs.syntaxHighlighting(true);
syntaxHighlighting.setValue(true, false);
new ModeInjector()
@@ -461,12 +452,9 @@
&& Objects.equals(mode, getSelectedMode())
&& view.isAttached()) {
view.operation(
- new Runnable() {
- @Override
- public void run() {
- view.getCmFromSide(DisplaySide.A).setOption("mode", mode);
- view.getCmFromSide(DisplaySide.B).setOption("mode", mode);
- }
+ () -> {
+ view.getCmFromSide(DisplaySide.A).setOption("mode", mode);
+ view.getCmFromSide(DisplaySide.B).setOption("mode", mode);
});
}
}
@@ -483,13 +471,10 @@
prefs.showWhitespaceErrors(e.getValue());
if (view != null) {
view.operation(
- new Runnable() {
- @Override
- public void run() {
- boolean s = prefs.showWhitespaceErrors();
- for (CodeMirror cm : view.getCms()) {
- cm.setOption("showTrailingSpace", s);
- }
+ () -> {
+ boolean s = prefs.showWhitespaceErrors();
+ for (CodeMirror cm : view.getCms()) {
+ cm.setOption("showTrailingSpace", s);
}
});
}
@@ -537,7 +522,7 @@
@UiHandler("theme")
void onTheme(@SuppressWarnings("unused") ChangeEvent e) {
- final Theme newTheme = getSelectedTheme();
+ Theme newTheme = getSelectedTheme();
prefs.theme(newTheme);
if (view != null) {
ThemeLoader.loadTheme(
@@ -546,15 +531,12 @@
@Override
public void onSuccess(Void result) {
view.operation(
- new Runnable() {
- @Override
- public void run() {
- if (getSelectedTheme() == newTheme && isAttached()) {
- String t = newTheme.name().toLowerCase();
- view.getCmFromSide(DisplaySide.A).setOption("theme", t);
- view.getCmFromSide(DisplaySide.B).setOption("theme", t);
- view.setThemeStyles(newTheme.isDark());
- }
+ () -> {
+ if (getSelectedTheme() == newTheme && isAttached()) {
+ String t = newTheme.name().toLowerCase();
+ view.getCmFromSide(DisplaySide.A).setOption("theme", t);
+ view.getCmFromSide(DisplaySide.B).setOption("theme", t);
+ view.setThemeStyles(newTheme.isDark());
}
});
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java
index 6cb9b6a..ecdac46 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java
@@ -64,12 +64,9 @@
refresh =
cmB.on(
"refresh",
- new Runnable() {
- @Override
- public void run() {
- if (updateScale()) {
- updatePosition();
- }
+ () -> {
+ if (updateScale()) {
+ updatePosition();
}
});
updateScale();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
index 1560597..f2b5fa6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
@@ -102,14 +102,11 @@
super.onShowView();
operation(
- new Runnable() {
- @Override
- public void run() {
- resizeCodeMirror();
- chunkManager.adjustPadding();
- cmA.refresh();
- cmB.refresh();
- }
+ () -> {
+ resizeCodeMirror();
+ chunkManager.adjustPadding();
+ cmA.refresh();
+ cmB.refresh();
});
setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
diffTable.refresh();
@@ -183,8 +180,8 @@
};
}
- private void display(final CommentsCollections comments) {
- final DiffInfo diff = getDiff();
+ private void display(CommentsCollections comments) {
+ DiffInfo diff = getDiff();
setThemeStyles(prefs.theme().isDark());
setShowIntraline(prefs.intralineDifference());
if (prefs.showLineNumbers()) {
@@ -209,18 +206,15 @@
chunkManager = new SideBySideChunkManager(this, cmA, cmB, diffTable.scrollbar);
operation(
- new Runnable() {
- @Override
- public void run() {
- // Estimate initial CodeMirror height, fixed up in onShowView.
- int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18);
- cmA.setHeight(height);
- cmB.setHeight(height);
+ () -> {
+ // Estimate initial CodeMirror height, fixed up in onShowView.
+ int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18);
+ cmA.setHeight(height);
+ cmB.setHeight(height);
- render(diff);
- commentManager.render(comments, prefs.expandAllComments());
- skipManager.render(prefs.context(), diff);
- }
+ render(diff);
+ commentManager.render(comments, prefs.expandAllComments());
+ skipManager.render(prefs.context(), diff);
});
registerCmEvents(cmA);
@@ -319,66 +313,52 @@
}
@Override
- Runnable updateActiveLine(final CodeMirror cm) {
- final CodeMirror other = otherCm(cm);
- return new Runnable() {
- @Override
- public void run() {
- // The rendering of active lines has to be deferred. Reflow
- // caused by adding and removing styles chokes Firefox when arrow
- // key (or j/k) is held down. Performance on Chrome is fine
- // without the deferral.
- //
- Scheduler.get()
- .scheduleDeferred(
- new ScheduledCommand() {
- @Override
- public void execute() {
- operation(
- new Runnable() {
- @Override
- public void run() {
- LineHandle handle =
- cm.getLineHandleVisualStart(cm.getCursor("end").line());
- if (!cm.extras().activeLine(handle)) {
- return;
- }
+ Runnable updateActiveLine(CodeMirror cm) {
+ CodeMirror other = otherCm(cm);
+ return () -> {
+ // The rendering of active lines has to be deferred. Reflow
+ // caused by adding and removing styles chokes Firefox when arrow
+ // key (or j/k) is held down. Performance on Chrome is fine
+ // without the deferral.
+ //
+ Scheduler.get()
+ .scheduleDeferred(
+ new ScheduledCommand() {
+ @Override
+ public void execute() {
+ operation(
+ () -> {
+ LineHandle handle = cm.getLineHandleVisualStart(cm.getCursor("end").line());
+ if (!cm.extras().activeLine(handle)) {
+ return;
+ }
- LineOnOtherInfo info = lineOnOther(cm.side(), cm.getLineNumber(handle));
- if (info.isAligned()) {
- other.extras().activeLine(other.getLineHandle(info.getLine()));
- } else {
- other.extras().clearActiveLine();
- }
- }
- });
- }
- });
- }
+ LineOnOtherInfo info = lineOnOther(cm.side(), cm.getLineNumber(handle));
+ if (info.isAligned()) {
+ other.extras().activeLine(other.getLineHandle(info.getLine()));
+ } else {
+ other.extras().clearActiveLine();
+ }
+ });
+ }
+ });
};
}
- private Runnable moveCursorToSide(final CodeMirror cmSrc, DisplaySide sideDst) {
- final CodeMirror cmDst = getCmFromSide(sideDst);
+ private Runnable moveCursorToSide(CodeMirror cmSrc, DisplaySide sideDst) {
+ CodeMirror cmDst = getCmFromSide(sideDst);
if (cmDst == cmSrc) {
- return new Runnable() {
- @Override
- public void run() {}
- };
+ return () -> {};
}
- final DisplaySide sideSrc = cmSrc.side();
- return new Runnable() {
- @Override
- public void run() {
- if (cmSrc.extras().hasActiveLine()) {
- cmDst.setCursor(
- Pos.create(
- lineOnOther(sideSrc, cmSrc.getLineNumber(cmSrc.extras().activeLine()))
- .getLine()));
- }
- cmDst.focus();
+ DisplaySide sideSrc = cmSrc.side();
+ return () -> {
+ if (cmSrc.extras().hasActiveLine()) {
+ cmDst.setCursor(
+ Pos.create(
+ lineOnOther(sideSrc, cmSrc.getLineNumber(cmSrc.extras().activeLine())).getLine()));
}
+ cmDst.focus();
};
}
@@ -389,20 +369,8 @@
}
@Override
- void operation(final Runnable apply) {
- cmA.operation(
- new Runnable() {
- @Override
- public void run() {
- cmB.operation(
- new Runnable() {
- @Override
- public void run() {
- apply.run();
- }
- });
- }
- });
+ void operation(Runnable apply) {
+ cmA.operation(() -> cmB.operation(apply::run));
}
@Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java
index a78e59e..cfd4226 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java
@@ -245,16 +245,13 @@
}
@Override
- Runnable diffChunkNav(final CodeMirror cm, final Direction dir) {
- return new Runnable() {
- @Override
- public void run() {
- int line = cm.extras().hasActiveLine() ? cm.getLineNumber(cm.extras().activeLine()) : 0;
- int res =
- Collections.binarySearch(
- chunks, new DiffChunkInfo(cm.side(), line, 0, false), getDiffChunkComparator());
- diffChunkNavHelper(chunks, host, res, dir);
- }
+ Runnable diffChunkNav(CodeMirror cm, Direction dir) {
+ return () -> {
+ int line = cm.extras().hasActiveLine() ? cm.getLineNumber(cm.extras().activeLine()) : 0;
+ int res =
+ Collections.binarySearch(
+ chunks, new DiffChunkInfo(cm.side(), line, 0, false), getDiffChunkComparator());
+ diffChunkNavHelper(chunks, host, res, dir);
};
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java
index 6fcd6c8..c728f6f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java
@@ -88,29 +88,26 @@
void handleRedraw() {
getLineWidget()
.onRedraw(
- new Runnable() {
- @Override
- public void run() {
- if (canComputeHeight() && peers.peek().canComputeHeight()) {
- if (getResizeTimer() != null) {
- getResizeTimer().cancel();
- setResizeTimer(null);
- }
- adjustPadding(SideBySideCommentGroup.this, peers.peek());
- } else if (getResizeTimer() == null) {
- setResizeTimer(
- new Timer() {
- @Override
- public void run() {
- if (canComputeHeight() && peers.peek().canComputeHeight()) {
- cancel();
- setResizeTimer(null);
- adjustPadding(SideBySideCommentGroup.this, peers.peek());
- }
- }
- });
- getResizeTimer().scheduleRepeating(5);
+ () -> {
+ if (canComputeHeight() && peers.peek().canComputeHeight()) {
+ if (getResizeTimer() != null) {
+ getResizeTimer().cancel();
+ setResizeTimer(null);
}
+ adjustPadding(SideBySideCommentGroup.this, peers.peek());
+ } else if (getResizeTimer() == null) {
+ setResizeTimer(
+ new Timer() {
+ @Override
+ public void run() {
+ if (canComputeHeight() && peers.peek().canComputeHeight()) {
+ cancel();
+ setResizeTimer(null);
+ adjustPadding(SideBySideCommentGroup.this, peers.peek());
+ }
+ }
+ });
+ getResizeTimer().scheduleRepeating(5);
}
});
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
index 7465c81..c65dcf0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
@@ -75,12 +75,7 @@
}
Runnable toggleA() {
- return new Runnable() {
- @Override
- public void run() {
- setVisibleA(!isVisibleA());
- }
- };
+ return () -> setVisibleA(!isVisibleA());
}
void setVisibleB(boolean show) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
index 03cfd60..eafb10f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
@@ -91,12 +91,9 @@
}
if (isNew) {
lineWidget.onFirstRedraw(
- new Runnable() {
- @Override
- public void run() {
- int w = cm.getGutterElement().getOffsetWidth();
- getElement().getStyle().setPaddingLeft(w, Unit.PX);
- }
+ () -> {
+ int w = cm.getGutterElement().getOffsetWidth();
+ getElement().getStyle().setPaddingLeft(w, Unit.PX);
});
}
}
@@ -110,14 +107,7 @@
.set("inclusiveLeft", true)
.set("inclusiveRight", true));
- textMarker.on(
- "beforeCursorEnter",
- new Runnable() {
- @Override
- public void run() {
- expandAll();
- }
- });
+ textMarker.on("beforeCursorEnter", this::expandAll);
int skipped = end - start + 1;
if (skipped <= UP_DOWN_THRESHOLD) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
index 0f0ba41..8647d68 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
@@ -30,7 +30,6 @@
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
@@ -102,12 +101,9 @@
super.onShowView();
operation(
- new Runnable() {
- @Override
- public void run() {
- resizeCodeMirror();
- cm.refresh();
- }
+ () -> {
+ resizeCodeMirror();
+ cm.refresh();
});
setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
diffTable.refresh();
@@ -137,18 +133,15 @@
}
@Override
- void registerCmEvents(final CodeMirror cm) {
+ void registerCmEvents(CodeMirror cm) {
super.registerCmEvents(cm);
cm.on(
"scroll",
- new Runnable() {
- @Override
- public void run() {
- ScrollInfo si = cm.getScrollInfo();
- if (autoHideDiffTableHeader) {
- updateDiffTableHeader(si);
- }
+ () -> {
+ ScrollInfo si = cm.getScrollInfo();
+ if (autoHideDiffTableHeader) {
+ updateDiffTableHeader(si);
}
});
maybeRegisterRenderEntireFileKeyMap(cm);
@@ -171,8 +164,8 @@
};
}
- private void display(final CommentsCollections comments) {
- final DiffInfo diff = getDiff();
+ private void display(CommentsCollections comments) {
+ DiffInfo diff = getDiff();
setThemeStyles(prefs.theme().isDark());
setShowIntraline(prefs.intralineDifference());
if (prefs.showLineNumbers()) {
@@ -186,17 +179,14 @@
chunkManager = new UnifiedChunkManager(this, cm, diffTable.scrollbar);
operation(
- new Runnable() {
- @Override
- public void run() {
- // Estimate initial CodeMirror height, fixed up in onShowView.
- int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18);
- cm.setHeight(height);
+ () -> {
+ // Estimate initial CodeMirror height, fixed up in onShowView.
+ int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18);
+ cm.setHeight(height);
- render(diff);
- commentManager.render(comments, prefs.expandAllComments());
- skipManager.render(prefs.context(), diff);
- }
+ render(diff);
+ commentManager.render(comments, prefs.expandAllComments());
+ skipManager.render(prefs.context(), diff);
});
registerCmEvents(cm);
@@ -317,25 +307,19 @@
}
@Override
- Runnable updateActiveLine(final CodeMirror cm) {
- return new Runnable() {
- @Override
- public void run() {
- // The rendering of active lines has to be deferred. Reflow
- // caused by adding and removing styles chokes Firefox when arrow
- // key (or j/k) is held down. Performance on Chrome is fine
- // without the deferral.
- //
- Scheduler.get()
- .scheduleDeferred(
- new ScheduledCommand() {
- @Override
- public void execute() {
- LineHandle handle = cm.getLineHandleVisualStart(cm.getCursor("end").line());
- cm.extras().activeLine(handle);
- }
- });
- }
+ Runnable updateActiveLine(CodeMirror cm) {
+ return () -> {
+ // The rendering of active lines has to be deferred. Reflow
+ // caused by adding and removing styles chokes Firefox when arrow
+ // key (or j/k) is held down. Performance on Chrome is fine
+ // without the deferral.
+ //
+ Scheduler.get()
+ .scheduleDeferred(
+ () -> {
+ LineHandle handle = cm.getLineHandleVisualStart(cm.getCursor("end").line());
+ cm.extras().activeLine(handle);
+ });
};
}
@@ -354,14 +338,8 @@
}
@Override
- void operation(final Runnable apply) {
- cm.operation(
- new Runnable() {
- @Override
- public void run() {
- apply.run();
- }
- });
+ void operation(Runnable apply) {
+ cm.operation(apply::run);
}
@Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
index 3939f99..1a662e2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
@@ -213,18 +213,15 @@
}
@Override
- Runnable diffChunkNav(final CodeMirror cm, final Direction dir) {
- return new Runnable() {
- @Override
- public void run() {
- int line = cm.extras().hasActiveLine() ? cm.getLineNumber(cm.extras().activeLine()) : 0;
- int res =
- Collections.binarySearch(
- chunks,
- new UnifiedDiffChunkInfo(cm.side(), 0, 0, line, false),
- getDiffChunkComparatorCmLine());
- diffChunkNavHelper(chunks, host, res, dir);
- }
+ Runnable diffChunkNav(CodeMirror cm, Direction dir) {
+ return () -> {
+ int line = cm.extras().hasActiveLine() ? cm.getLineNumber(cm.extras().activeLine()) : 0;
+ int res =
+ Collections.binarySearch(
+ chunks,
+ new UnifiedDiffChunkInfo(cm.side(), 0, 0, line, false),
+ getDiffChunkComparatorCmLine());
+ diffChunkNavHelper(chunks, host, res, dir);
};
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java
index a6912df..6d5fba3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java
@@ -50,29 +50,26 @@
void handleRedraw() {
getLineWidget()
.onRedraw(
- new Runnable() {
- @Override
- public void run() {
- if (canComputeHeight()) {
- if (getResizeTimer() != null) {
- getResizeTimer().cancel();
- setResizeTimer(null);
- }
- reportHeightChange();
- } else if (getResizeTimer() == null) {
- setResizeTimer(
- new Timer() {
- @Override
- public void run() {
- if (canComputeHeight()) {
- cancel();
- setResizeTimer(null);
- reportHeightChange();
- }
- }
- });
- getResizeTimer().scheduleRepeating(5);
+ () -> {
+ if (canComputeHeight()) {
+ if (getResizeTimer() != null) {
+ getResizeTimer().cancel();
+ setResizeTimer(null);
}
+ reportHeightChange();
+ } else if (getResizeTimer() == null) {
+ setResizeTimer(
+ new Timer() {
+ @Override
+ public void run() {
+ if (canComputeHeight()) {
+ cancel();
+ setResizeTimer(null);
+ reportHeightChange();
+ }
+ }
+ });
+ getResizeTimer().scheduleRepeating(5);
}
});
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index 511944b..3cf00c9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -49,7 +49,6 @@
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
@@ -318,12 +317,7 @@
}
private Runnable gotoLine() {
- return new Runnable() {
- @Override
- public void run() {
- cmEdit.execCommand("jumpToLine");
- }
- };
+ return () -> cmEdit.execCommand("jumpToLine");
}
@Override
@@ -472,21 +466,9 @@
cmEdit.setOption(option, value);
}
- void setTheme(final Theme newTheme) {
- cmBase.operation(
- new Runnable() {
- @Override
- public void run() {
- cmBase.setOption("theme", newTheme.name().toLowerCase());
- }
- });
- cmEdit.operation(
- new Runnable() {
- @Override
- public void run() {
- cmEdit.setOption("theme", newTheme.name().toLowerCase());
- }
- });
+ void setTheme(Theme newTheme) {
+ cmBase.operation(() -> cmBase.setOption("theme", newTheme.name().toLowerCase()));
+ cmEdit.operation(() -> cmEdit.setOption("theme", newTheme.name().toLowerCase()));
}
void setLineLength(int length) {
@@ -504,21 +486,9 @@
cmEdit.setOption("lineNumbers", show);
}
- void setShowWhitespaceErrors(final boolean show) {
- cmBase.operation(
- new Runnable() {
- @Override
- public void run() {
- cmBase.setOption("showTrailingSpace", show);
- }
- });
- cmEdit.operation(
- new Runnable() {
- @Override
- public void run() {
- cmEdit.setOption("showTrailingSpace", show);
- }
- });
+ void setShowWhitespaceErrors(boolean show) {
+ cmBase.operation(() -> cmBase.setOption("showTrailingSpace", show));
+ cmEdit.operation(() -> cmEdit.setOption("showTrailingSpace", show));
}
void setShowTabs(boolean show) {
@@ -643,29 +613,13 @@
}
private Runnable updateCursorPosition() {
- return new Runnable() {
- @Override
- public void run() {
- // The rendering of active lines has to be deferred. Reflow
- // caused by adding and removing styles chokes Firefox when arrow
- // key (or j/k) is held down. Performance on Chrome is fine
- // without the deferral.
- //
- Scheduler.get()
- .scheduleDeferred(
- new ScheduledCommand() {
- @Override
- public void execute() {
- cmEdit.operation(
- new Runnable() {
- @Override
- public void run() {
- updateActiveLine();
- }
- });
- }
- });
- }
+ return () -> {
+ // The rendering of active lines has to be deferred. Reflow
+ // caused by adding and removing styles chokes Firefox when arrow
+ // key (or j/k) is held down. Performance on Chrome is fine
+ // without the deferral.
+ //
+ Scheduler.get().scheduleDeferred(() -> cmEdit.operation(this::updateActiveLine));
};
}
@@ -683,37 +637,34 @@
}
private Runnable save() {
- return new Runnable() {
- @Override
- public void run() {
- if (!cmEdit.isClean(generation)) {
- close.setEnabled(false);
- String text = cmEdit.getValue();
- if (Patch.COMMIT_MSG.equals(path)) {
- String trimmed = text.trim() + "\r";
- if (!trimmed.equals(text)) {
- text = trimmed;
- cmEdit.setValue(text);
- }
+ return () -> {
+ if (!cmEdit.isClean(generation)) {
+ close.setEnabled(false);
+ String text = cmEdit.getValue();
+ if (Patch.COMMIT_MSG.equals(path)) {
+ String trimmed = text.trim() + "\r";
+ if (!trimmed.equals(text)) {
+ text = trimmed;
+ cmEdit.setValue(text);
}
- final int g = cmEdit.changeGeneration(false);
- ChangeEditApi.put(
- revision.getParentKey().get(),
- path,
- text,
- new GerritCallback<VoidResult>() {
- @Override
- public void onSuccess(VoidResult result) {
- generation = g;
- setClean(cmEdit.isClean(g));
- }
-
- @Override
- public void onFailure(final Throwable caught) {
- close.setEnabled(true);
- }
- });
}
+ final int g = cmEdit.changeGeneration(false);
+ ChangeEditApi.put(
+ revision.getParentKey().get(),
+ path,
+ text,
+ new GerritCallback<VoidResult>() {
+ @Override
+ public void onSuccess(VoidResult result) {
+ generation = g;
+ setClean(cmEdit.isClean(g));
+ }
+
+ @Override
+ public void onFailure(final Throwable caught) {
+ close.setEnabled(true);
+ }
+ });
}
};
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index 738319d..b889ff7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -57,6 +57,9 @@
public final native InheritedBooleanInfo rejectImplicitMerges()
/*-{ return this.reject_implicit_merges; }-*/ ;
+ public final native InheritedBooleanInfo enableReviewerByEmail()
+ /*-{ return this.enable_reviewer_by_email; }-*/ ;
+
public final SubmitType submitType() {
return SubmitType.valueOf(submitTypeRaw());
}
@@ -113,6 +116,9 @@
final native ThemeInfo theme() /*-{ return this.theme; }-*/;
+ final native NativeMap<JsArrayString>
+ extensionPanelNames() /*-{ return this.extension_panel_names; }-*/;
+
protected ConfigInfo() {}
static class CommentLinkInfo extends JavaScriptObject {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
index e41cf120..7182b78 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
@@ -16,12 +16,14 @@
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.CommentLinkProcessor;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
/** Cache of {@link ConfigInfo} objects by project name. */
@@ -48,6 +50,10 @@
public ThemeInfo getTheme() {
return info.theme();
}
+
+ public List<String> getExtensionPanelNames(String extensionPoint) {
+ return Natives.asList(info.extensionPanelNames().get(extensionPoint));
+ }
}
public static void get(Project.NameKey name, AsyncCallback<Entry> cb) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index 4be877e..71fa007 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -147,6 +147,7 @@
InheritableBoolean enableSignedPush,
InheritableBoolean requireSignedPush,
InheritableBoolean rejectImplicitMerges,
+ InheritableBoolean enableReviewerByEmail,
String maxObjectSizeLimit,
SubmitType submitType,
ProjectState state,
@@ -170,6 +171,7 @@
in.setSubmitType(submitType);
in.setState(state);
in.setPluginConfigValues(pluginConfigValues);
+ in.setEnableReviewerByEmail(enableReviewerByEmail);
project(name).view("config").put(in, cb);
}
@@ -294,6 +296,13 @@
setRequireSignedPushRaw(v.name());
}
+ final void setEnableReviewerByEmail(InheritableBoolean v) {
+ setEnableReviewerByEmailRaw(v.name());
+ }
+
+ private native void setEnableReviewerByEmailRaw(String v)
+ /*-{ if(v)this.enable_reviewer_by_email=v; }-*/ ;
+
private native void setRequireSignedPushRaw(String v)
/*-{ if(v)this.require_signed_push=v; }-*/ ;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 9676cd3..f7309ec 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -27,7 +27,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.Provider;
import com.google.inject.servlet.RequestScoped;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
index f1600bc..e476f15 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
@@ -19,7 +19,7 @@
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
public interface WebSession {
boolean isSignedIn();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
index 59591cc..7884089 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -30,7 +30,7 @@
import com.google.common.cache.Cache;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index b7c6be3..7f6255a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -14,8 +14,8 @@
package com.google.gerrit.httpd.auth.become;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_UUID;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.extensions.registration.DynamicItem;
@@ -30,7 +30,7 @@
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.gwtorm.server.OrmException;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index 5a0ed71..3a575a1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -17,7 +17,7 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -26,7 +26,7 @@
import com.google.gerrit.httpd.RemoteUserUtil;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.httpd.raw.HostPageServlet;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.gwtjsonrpc.server.RPCServletUtils;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index 40b543b..3696c21 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -14,7 +14,7 @@
package com.google.gerrit.httpd.auth.container;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_EXTERNAL;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_EXTERNAL;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.gerrit.common.PageLinks;
@@ -27,7 +27,7 @@
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.gwtorm.server.OrmException;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
index b48caf5..1491345 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
@@ -14,9 +14,11 @@
package com.google.gerrit.httpd.plugins;
+import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.httpd.resources.Resource;
import com.google.gerrit.httpd.resources.ResourceKey;
import com.google.gerrit.httpd.resources.ResourceWeigher;
+import com.google.gerrit.server.DynamicOptions;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.plugins.ModuleGenerator;
import com.google.gerrit.server.plugins.ReloadPluginListener;
@@ -62,5 +64,7 @@
.weigher(ResourceWeigher.class);
}
});
+
+ DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 9730032..039fcb3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -64,6 +64,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentMap;
@@ -637,7 +638,11 @@
Path path = plugin.getSrcFile();
if (req.getRequestURI().endsWith(getJsPluginPath(plugin)) && Files.exists(path)) {
res.setHeader("Content-Length", Long.toString(Files.size(path)));
- res.setContentType("application/javascript");
+ if (path.toString().toLowerCase(Locale.US).endsWith(".html")) {
+ res.setContentType("text/html");
+ } else {
+ res.setContentType("application/javascript");
+ }
writeToResponse(res, Files.newInputStream(path));
} else {
resourceCache.put(key, Resource.NOT_FOUND);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
index 7e298aa..298301d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -35,6 +35,7 @@
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.lib.ObjectId;
/**
* Exports a single version of a patch as a normal file download.
@@ -126,7 +127,7 @@
try {
Optional<ChangeEdit> edit = changeEditUtil.byChange(control.getChange());
if (edit.isPresent()) {
- revision = edit.get().getRevision().get();
+ revision = ObjectId.toString(edit.get().getEditCommit());
} else {
rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
index ced3121..3e1913d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -24,9 +24,11 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
+import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.server.DynamicOptions;
import com.google.gerrit.util.cli.CmdLineParser;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@@ -49,16 +51,19 @@
ImmutableSet.of("pp", "prettyPrint", "strict", "callback", "alt", "fields");
private final CmdLineParser.Factory parserFactory;
+ private final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
@Inject
- ParameterParser(CmdLineParser.Factory pf) {
+ ParameterParser(CmdLineParser.Factory pf, DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
this.parserFactory = pf;
+ this.dynamicBeans = dynamicBeans;
}
<T> boolean parse(
T param, ListMultimap<String, String> in, HttpServletRequest req, HttpServletResponse res)
throws IOException {
CmdLineParser clp = parserFactory.create(param);
+ DynamicOptions.parse(dynamicBeans, clp, param);
try {
clp.parseOptionMap(in);
} catch (CmdLineException | NumberFormatException e) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index d9dd5d4..abf5323 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -119,6 +119,7 @@
import java.io.EOFException;
import java.io.FilterOutputStream;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
@@ -144,6 +145,7 @@
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.eclipse.jgit.http.server.ServletUtils;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.TemporaryBuffer.Heap;
@@ -371,8 +373,10 @@
RestModifyView<RestResource, Object> m =
(RestModifyView<RestResource, Object>) viewData.view;
- inputRequestBody = parseRequest(req, inputType(m));
+ Type type = inputType(m);
+ inputRequestBody = parseRequest(req, type);
result = m.apply(rsrc, inputRequestBody);
+ consumeRawInputRequestBody(req, type);
} else {
throw new ResourceNotFoundException();
}
@@ -666,24 +670,30 @@
throws IOException, BadRequestException, SecurityException, IllegalArgumentException,
NoSuchMethodException, IllegalAccessException, InstantiationException,
InvocationTargetException, MethodNotAllowedException {
+ // HTTP/1.1 requires consuming the request body before writing non-error response (less than
+ // 400). Consume the request body for all but raw input request types here.
if (isType(JSON_TYPE, req.getContentType())) {
try (BufferedReader br = req.getReader();
JsonReader json = new JsonReader(br)) {
- json.setLenient(true);
-
- JsonToken first;
try {
- first = json.peek();
- } catch (EOFException e) {
- throw new BadRequestException("Expected JSON object");
+ json.setLenient(true);
+
+ JsonToken first;
+ try {
+ first = json.peek();
+ } catch (EOFException e) {
+ throw new BadRequestException("Expected JSON object");
+ }
+ if (first == JsonToken.STRING) {
+ return parseString(json.nextString(), type);
+ }
+ return OutputFormat.JSON.newGson().fromJson(json, type);
+ } finally {
+ // Reader.close won't consume the rest of the input. Explicitly consume the request body.
+ br.skip(Long.MAX_VALUE);
}
- if (first == JsonToken.STRING) {
- return parseString(json.nextString(), type);
- }
- return OutputFormat.JSON.newGson().fromJson(json, type);
}
- } else if (("PUT".equals(req.getMethod()) || "POST".equals(req.getMethod()))
- && acceptsRawInput(type)) {
+ } else if (rawInputRequest(req, type)) {
return parseRawInput(req, type);
} else if ("DELETE".equals(req.getMethod()) && hasNoBody(req)) {
return null;
@@ -706,6 +716,19 @@
}
}
+ private void consumeRawInputRequestBody(HttpServletRequest req, Type type) throws IOException {
+ if (rawInputRequest(req, type)) {
+ try (InputStream is = req.getInputStream()) {
+ ServletUtils.consumeRequestBody(is);
+ }
+ }
+ }
+
+ private static boolean rawInputRequest(HttpServletRequest req, Type type) {
+ String method = req.getMethod();
+ return ("PUT".equals(method) || "POST".equals(method)) && acceptsRawInput(type);
+ }
+
private static boolean hasNoBody(HttpServletRequest req) {
int len = req.getContentLength();
String type = req.getContentType();
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
index 5c3183a..9e375e7 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -20,7 +20,6 @@
import com.google.common.base.Joiner;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.AbstractFuture;
-import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
@@ -98,7 +97,7 @@
this.sitePaths = sitePaths;
this.dir = dir;
this.name = name;
- final String index = Joiner.on('_').skipNulls().join(name, subIndex);
+ String index = Joiner.on('_').skipNulls().join(name, subIndex);
IndexWriter delegateWriter;
long commitPeriod = writerConfig.getCommitWithinMs();
@@ -121,28 +120,25 @@
@SuppressWarnings("unused") // Error handling within Runnable.
Future<?> possiblyIgnoredError =
autoCommitExecutor.scheduleAtFixedRate(
- new Runnable() {
- @Override
- public void run() {
+ () -> {
+ try {
+ if (autoCommitWriter.hasUncommittedChanges()) {
+ autoCommitWriter.manualFlush();
+ autoCommitWriter.commit();
+ }
+ } catch (IOException e) {
+ log.error("Error committing " + index + " Lucene index", e);
+ } catch (OutOfMemoryError e) {
+ log.error("Error committing " + index + " Lucene index", e);
try {
- if (autoCommitWriter.hasUncommittedChanges()) {
- autoCommitWriter.manualFlush();
- autoCommitWriter.commit();
- }
- } catch (IOException e) {
- log.error("Error committing " + index + " Lucene index", e);
- } catch (OutOfMemoryError e) {
- log.error("Error committing " + index + " Lucene index", e);
- try {
- autoCommitWriter.close();
- } catch (IOException e2) {
- log.error(
- "SEVERE: Error closing "
- + index
- + " Lucene index after OOM;"
- + " index may be corrupted.",
- e);
- }
+ autoCommitWriter.close();
+ } catch (IOException e2) {
+ log.error(
+ "SEVERE: Error closing "
+ + index
+ + " Lucene index after OOM;"
+ + " index may be corrupted.",
+ e);
}
}
},
@@ -247,48 +243,27 @@
}
}
- ListenableFuture<?> insert(final Document doc) {
- return submit(
- new Callable<Long>() {
- @Override
- public Long call() throws IOException, InterruptedException {
- return writer.addDocument(doc);
- }
- });
+ ListenableFuture<?> insert(Document doc) {
+ return submit(() -> writer.addDocument(doc));
}
- ListenableFuture<?> replace(final Term term, final Document doc) {
- return submit(
- new Callable<Long>() {
- @Override
- public Long call() throws IOException, InterruptedException {
- return writer.updateDocument(term, doc);
- }
- });
+ ListenableFuture<?> replace(Term term, Document doc) {
+ return submit(() -> writer.updateDocument(term, doc));
}
- ListenableFuture<?> delete(final Term term) {
- return submit(
- new Callable<Long>() {
- @Override
- public Long call() throws IOException, InterruptedException {
- return writer.deleteDocuments(term);
- }
- });
+ ListenableFuture<?> delete(Term term) {
+ return submit(() -> writer.deleteDocuments(term));
}
private ListenableFuture<?> submit(Callable<Long> task) {
ListenableFuture<Long> future = Futures.nonCancellationPropagating(writerThread.submit(task));
return Futures.transformAsync(
future,
- new AsyncFunction<Long, Void>() {
- @Override
- public ListenableFuture<Void> apply(Long gen) throws InterruptedException {
- // Tell the reopen thread a future is waiting on this
- // generation so it uses the min stale time when refreshing.
- reopenThread.waitForGeneration(gen, 0);
- return new NrtFuture(gen);
- }
+ gen -> {
+ // Tell the reopen thread a future is waiting on this
+ // generation so it uses the min stale time when refreshing.
+ reopenThread.waitForGeneration(gen, 0);
+ return new NrtFuture(gen);
},
directExecutor());
}
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 96986a9..3afcb07 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -61,9 +61,9 @@
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.OrmRuntimeException;
import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -121,6 +121,7 @@
private static final String REF_STATE_PATTERN_FIELD = ChangeField.REF_STATE_PATTERN.getName();
private static final String REVIEWEDBY_FIELD = ChangeField.REVIEWEDBY.getName();
private static final String REVIEWER_FIELD = ChangeField.REVIEWER.getName();
+ private static final String REVIEWER_BY_EMAIL_FIELD = ChangeField.REVIEWER_BY_EMAIL.getName();
private static final String HASHTAG_FIELD = ChangeField.HASHTAG_CASE_AWARE.getName();
private static final String STAR_FIELD = ChangeField.STAR.getName();
private static final String SUBMIT_RECORD_LENIENT_FIELD =
@@ -147,7 +148,7 @@
private final ChangeSubIndex openIndex;
private final ChangeSubIndex closedIndex;
- @AssistedInject
+ @Inject
LuceneChangeIndex(
@GerritServerConfig Config cfg,
SitePaths sitePaths,
@@ -459,6 +460,9 @@
if (fields.contains(REVIEWER_FIELD)) {
decodeReviewers(doc, cd);
}
+ if (fields.contains(REVIEWER_BY_EMAIL_FIELD)) {
+ decodeReviewersByEmail(doc, cd);
+ }
decodeSubmitRecords(
doc, SUBMIT_RECORD_STRICT_FIELD, ChangeField.SUBMIT_RULE_OPTIONS_STRICT, cd);
decodeSubmitRecords(
@@ -555,6 +559,13 @@
FluentIterable.from(doc.get(REVIEWER_FIELD)).transform(IndexableField::stringValue)));
}
+ private void decodeReviewersByEmail(ListMultimap<String, IndexableField> doc, ChangeData cd) {
+ cd.setReviewersByEmail(
+ ChangeField.parseReviewerByEmailFieldValues(
+ FluentIterable.from(doc.get(REVIEWER_BY_EMAIL_FIELD))
+ .transform(IndexableField::stringValue)));
+ }
+
private void decodeSubmitRecords(
ListMultimap<String, IndexableField> doc,
String field,
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
index 0391831..3cb8816 100644
--- a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
+++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -31,7 +31,7 @@
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
index e862bac..878f9ee 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -31,7 +31,7 @@
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index efe8c5f..a3bf361 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -26,7 +26,7 @@
import com.google.gerrit.server.UrlEncoded;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.ConfigUtil;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
index 22f0f3e..475ff2b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -228,12 +228,9 @@
try {
start();
RuntimeShutdown.add(
- new Runnable() {
- @Override
- public void run() {
- log.info("caught shutdown, cleaning up");
- stop();
- }
+ () -> {
+ log.info("caught shutdown, cleaning up");
+ stop();
});
log.info("Gerrit Code Review " + myVersion() + " ready");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
index b1a50d7..004486b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
@@ -45,16 +45,13 @@
manager.add(dbInjector);
manager.start();
RuntimeShutdown.add(
- new Runnable() {
- @Override
- public void run() {
- try {
- System.in.close();
- } catch (IOException e) {
- // Ignored
- }
- manager.stop();
+ () -> {
+ try {
+ System.in.close();
+ } catch (IOException e) {
+ // Ignored
}
+ manager.stop();
});
final QueryShell shell = shellFactory().create(System.in, System.out);
shell.setOutputFormat(format);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
index 7457f40..4e18ddc 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -14,16 +14,19 @@
package com.google.gerrit.pgm;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.account.ExternalId;
-import com.google.gerrit.server.account.ExternalIdsBatchUpdate;
+import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.ExternalIdsBatchUpdate;
import com.google.gerrit.server.schema.SchemaVersionCheck;
import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Injector;
import java.util.Collection;
@@ -37,6 +40,8 @@
@Inject private SchemaFactory<ReviewDb> database;
+ @Inject private ExternalIds externalIds;
+
@Inject private ExternalIdsBatchUpdate externalIdsBatchUpdate;
@Override
@@ -44,10 +49,22 @@
Injector dbInjector = createDbInjector(MULTI_USER);
manager.add(dbInjector, dbInjector.createChildInjector(SchemaVersionCheck.module()));
manager.start();
- dbInjector.injectMembers(this);
+ dbInjector
+ .createChildInjector(
+ new AbstractModule() {
+ @Override
+ protected void configure() {
+ // The LocalUsernamesToLowerCase program needs to access all external IDs only
+ // once to update them. After the update they are not accessed again. Hence the
+ // LocalUsernamesToLowerCase program doesn't benefit from caching external IDs and
+ // the external ID cache can be disabled.
+ install(DisabledExternalIdCache.module());
+ }
+ })
+ .injectMembers(this);
try (ReviewDb db = database.open()) {
- Collection<ExternalId> todo = ExternalId.from(db.accountExternalIds().all().toList());
+ Collection<ExternalId> todo = externalIds.all(db);
monitor.beginTask("Converting local usernames", todo.size());
for (ExternalId extId : todo) {
@@ -56,9 +73,9 @@
}
externalIdsBatchUpdate.commit(db, "Convert local usernames to lower case");
+ monitor.endTask();
+ manager.stop();
}
- monitor.endTask();
- manager.stop();
return 0;
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
index 07e7921..fb524a3 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
@@ -85,13 +85,7 @@
Injector dbInjector = createDbInjector(SINGLE_USER);
manager.add(dbInjector);
manager.start();
- RuntimeShutdown.add(
- new Runnable() {
- @Override
- public void run() {
- manager.stop();
- }
- });
+ RuntimeShutdown.add(manager::stop);
dbInjector.injectMembers(this);
ProgressMonitor progress = new TextProgressMonitor();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
index d77717e..21daa3e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java
@@ -66,7 +66,6 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.lib.BatchRefUpdate;
@@ -153,18 +152,15 @@
List<ListenableFuture<Boolean>> futures = new ArrayList<>();
List<Project.NameKey> projectNames =
Ordering.usingToString().sortedCopy(changesByProject.keySet());
- for (final Project.NameKey project : projectNames) {
+ for (Project.NameKey project : projectNames) {
ListenableFuture<Boolean> future =
executor.submit(
- new Callable<Boolean>() {
- @Override
- public Boolean call() {
- try (ReviewDb db = unwrapDb(schemaFactory.open())) {
- return rebuildProject(db, changesByProject, project, allUsersRepo);
- } catch (Exception e) {
- log.error("Error rebuilding project " + project, e);
- return false;
- }
+ () -> {
+ try (ReviewDb db = unwrapDb(schemaFactory.open())) {
+ return rebuildProject(db, changesByProject, project, allUsersRepo);
+ } catch (Exception e) {
+ log.error("Error rebuilding project " + project, e);
+ return false;
}
});
futures.add(future);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
index 86c5f45e..5bedb1b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
@@ -14,14 +14,14 @@
package com.google.gerrit.pgm.init;
-import static com.google.gerrit.server.account.ExternalId.toAccountExternalIds;
+import static com.google.gerrit.server.account.externalids.ExternalId.toAccountExternalIds;
import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdentProvider;
-import com.google.gerrit.server.account.ExternalId;
-import com.google.gerrit.server.account.ExternalIds;
-import com.google.gerrit.server.account.ExternalIdsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdReader;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.config.SitePaths;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -61,9 +61,9 @@
try (Repository repo = new FileRepository(path);
RevWalk rw = new RevWalk(repo);
ObjectInserter ins = repo.newObjectInserter()) {
- ObjectId rev = ExternalIds.readRevision(repo);
+ ObjectId rev = ExternalIdReader.readRevision(repo);
- NoteMap noteMap = ExternalIds.readNoteMap(rw, rev);
+ NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
for (ExternalId extId : extIds) {
ExternalIdsUpdate.insert(rw, ins, noteMap, extId);
}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 68b2b96..529b7e7 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -29,7 +29,8 @@
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gwtorm.server.SchemaFactory;
@@ -47,6 +48,7 @@
public class InitAdminUser implements InitStep {
private final ConsoleUI ui;
private final InitFlags flags;
+ private final AccountsUpdate accountsUpdate;
private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
private final ExternalIdsOnInit externalIds;
private SchemaFactory<ReviewDb> dbFactory;
@@ -56,10 +58,12 @@
InitAdminUser(
InitFlags flags,
ConsoleUI ui,
+ AccountsUpdate accountsUpdate,
VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory,
ExternalIdsOnInit externalIds) {
this.flags = flags;
this.ui = ui;
+ this.accountsUpdate = accountsUpdate;
this.authorizedKeysFactory = authorizedKeysFactory;
this.externalIds = externalIds;
}
@@ -106,7 +110,7 @@
Account a = new Account(id, TimeUtil.nowTs());
a.setFullName(name);
a.setPreferredEmail(email);
- db.accounts().insert(Collections.singleton(a));
+ accountsUpdate.insert(db, a);
AccountGroupName adminGroupName =
db.accountGroupNames().get(new AccountGroup.NameKey("Administrators"));
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index aec0731..c86d5af 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -37,6 +37,7 @@
import com.google.gerrit.server.account.GroupCacheImpl;
import com.google.gerrit.server.account.GroupIncludeCacheImpl;
import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.account.externalids.ExternalIdModule;
import com.google.gerrit.server.cache.CacheRemovalListener;
import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
import com.google.gerrit.server.change.ChangeJson;
@@ -61,10 +62,9 @@
import com.google.gerrit.server.notedb.NoteDbModule;
import com.google.gerrit.server.patch.DiffExecutorModule;
import com.google.gerrit.server.patch.PatchListCacheImpl;
-import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.CommentLinkProvider;
+import com.google.gerrit.server.project.DefaultPermissionBackendModule;
import com.google.gerrit.server.project.ProjectCacheImpl;
-import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SectionSortCache;
import com.google.gerrit.server.query.change.ChangeData;
@@ -141,11 +141,11 @@
bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
.annotatedWith(GitReceivePackGroups.class)
.toInstance(Collections.<AccountGroup.UUID>emptySet());
- bind(ChangeControl.Factory.class);
- factory(ProjectControl.AssistedFactory.class);
install(new BatchGitModule());
+ install(new DefaultPermissionBackendModule());
install(new DefaultCacheFactory.Module());
+ install(new ExternalIdModule());
install(new GroupModule());
install(new NoteDbModule(cfg));
install(new PrologModule());
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 9309921..6a83e4d 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-api</artifactId>
- <version>2.14-SNAPSHOT</version>
+ <version>2.15-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Plugin API</name>
<description>API for Gerrit Plugins</description>
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
index 4b104c6..964e0d2 100644
--- a/gerrit-plugin-gwtui/pom.xml
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-gwtui</artifactId>
- <version>2.14-SNAPSHOT</version>
+ <version>2.15-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Plugin GWT UI</name>
<description>Common Classes for Gerrit GWT UI Plugins</description>
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
index 7c478c1..bfcb3d6 100644
--- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
@@ -121,12 +121,15 @@
*
* @param extensionPoint the UI extension point for which the panel should be registered.
* @param entry callback function invoked to create the panel widgets.
+ * @param name the name of the panel which can be used to specify panel ordering via project
+ * config
*/
- public void panel(GerritUiExtensionPoint extensionPoint, Panel.EntryPoint entry) {
- panel(extensionPoint.name(), wrap(entry));
+ public final void panel(
+ GerritUiExtensionPoint extensionPoint, Panel.EntryPoint entry, String name) {
+ panel(extensionPoint.name(), wrap(entry), name);
}
- private native void panel(String i, JavaScriptObject e) /*-{ this.panel(i, e) }-*/;
+ private native void panel(String i, JavaScriptObject e, String n) /*-{ this.panel(i, e, n) }-*/;
protected Plugin() {}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 9655edd..6495cf1 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -512,6 +512,10 @@
@Column(id = 19, notNull = false)
protected Account.Id assignee;
+ /** Whether the change is private. */
+ @Column(id = 20)
+ protected boolean isPrivate;
+
/** @see com.google.gerrit.server.notedb.NoteDbChangeState */
@Column(id = 101, notNull = false, length = Integer.MAX_VALUE)
protected String noteDbState;
@@ -548,6 +552,7 @@
originalSubject = other.originalSubject;
submissionId = other.submissionId;
topic = other.topic;
+ isPrivate = other.isPrivate;
noteDbState = other.noteDbState;
}
@@ -694,6 +699,14 @@
this.topic = topic;
}
+ public boolean isPrivate() {
+ return isPrivate;
+ }
+
+ public void setPrivate(boolean isPrivate) {
+ this.isPrivate = isPrivate;
+ }
+
public String getNoteDbState() {
return noteDbState;
}
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 cadd52c..4b3c652 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
@@ -15,6 +15,7 @@
package com.google.gerrit.reviewdb.client;
import java.sql.Timestamp;
+import java.util.Comparator;
import java.util.Objects;
/**
@@ -130,7 +131,13 @@
}
}
- public static class Range {
+ public static class Range implements Comparable<Range> {
+ private static final Comparator<Range> RANGE_COMPARATOR =
+ Comparator.<Range>comparingInt(range -> range.startLine)
+ .thenComparingInt(range -> range.startChar)
+ .thenComparingInt(range -> range.endLine)
+ .thenComparingInt(range -> range.endChar);
+
public int startLine; // 1-based, inclusive
public int startChar; // 0-based, inclusive
public int endLine; // 1-based, exclusive
@@ -186,6 +193,11 @@
.append('}')
.toString();
}
+
+ @Override
+ public int compareTo(Range otherRange) {
+ return RANGE_COMPARATOR.compare(this, otherRange);
+ }
}
public Key key;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index ba83c58..9918317 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -99,6 +99,8 @@
protected InheritableBoolean rejectImplicitMerges;
+ protected InheritableBoolean enableReviewerByEmail;
+
protected Project() {}
public Project(Project.NameKey nameKey) {
@@ -112,6 +114,7 @@
createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT;
enableSignedPush = InheritableBoolean.INHERIT;
requireSignedPush = InheritableBoolean.INHERIT;
+ enableReviewerByEmail = InheritableBoolean.INHERIT;
}
public Project.NameKey getNameKey() {
@@ -154,6 +157,14 @@
return rejectImplicitMerges;
}
+ public InheritableBoolean getEnableReviewerByEmail() {
+ return enableReviewerByEmail;
+ }
+
+ public void setEnableReviewerByEmail(final InheritableBoolean enable) {
+ enableReviewerByEmail = enable;
+ }
+
public void setUseContributorAgreements(final InheritableBoolean u) {
useContributorAgreements = u;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
index 9124301..e21faaf 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
@@ -30,6 +30,9 @@
@Query("WHERE accountId = ?")
ResultSet<AccountExternalId> byAccount(Account.Id id) throws OrmException;
+ @Query("WHERE emailAddress = ?")
+ ResultSet<AccountExternalId> byEmailAddress(String email) throws OrmException;
+
@Query
ResultSet<AccountExternalId> all() throws OrmException;
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 9b4e1ed..49b9337 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -118,4 +118,8 @@
@Sequence(startWith = FIRST_CHANGE_ID)
@Deprecated
int nextChangeId() throws OrmException;
+
+ default boolean changesTablesEnabled() {
+ return true;
+ }
}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index 6dd3701..8aeb3ad 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -158,6 +158,11 @@
return delegate.nextChangeId();
}
+ @Override
+ public boolean changesTablesEnabled() {
+ return delegate.changesTablesEnabled();
+ }
+
public static class ChangeAccessWrapper implements ChangeAccess {
protected final ChangeAccess delegate;
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index deceab9..2fcf53c 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -21,6 +21,9 @@
CREATE INDEX account_external_ids_byAccount
ON account_external_ids (account_id);
+-- covers: byEmailAddress
+CREATE INDEX account_external_ids_byEmail
+ON account_external_ids (email_address);
-- *********************************************************************
-- AccountGroupMemberAccess
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
index 1ec8ea6..3be3c26 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
@@ -25,6 +25,10 @@
ON account_external_ids (account_id)
#
+-- covers: byEmailAddress
+CREATE INDEX account_external_ids_byEmail
+ON account_external_ids (email_address)
+#
-- *********************************************************************
-- AccountGroupMemberAccess
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index a11c86b..641c613 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -68,6 +68,10 @@
CREATE INDEX account_external_ids_byAccount
ON account_external_ids (account_id);
+-- covers: byEmailAddress
+CREATE INDEX account_external_ids_byEmail
+ON account_external_ids (email_address);
+
-- *********************************************************************
-- AccountGroupMemberAccess
diff --git a/gerrit-server/BUILD b/gerrit-server/BUILD
index adfe7a4..8182237 100644
--- a/gerrit-server/BUILD
+++ b/gerrit-server/BUILD
@@ -138,6 +138,22 @@
],
)
+CUSTOM_TRUTH_SUBJECTS = glob([
+ "src/test/java/com/google/gerrit/server/**/*Subject.java",
+])
+
+java_library(
+ name = "custom-truth-subjects",
+ testonly = 1,
+ srcs = CUSTOM_TRUTH_SUBJECTS,
+ deps = [
+ ":server",
+ "//gerrit-extension-api:api",
+ "//gerrit-test-util:test_util",
+ "//lib:truth",
+ ],
+)
+
PROLOG_TEST_CASE = [
"src/test/java/com/google/gerrit/rules/PrologTestCase.java",
]
@@ -213,11 +229,12 @@
size = "large",
srcs = glob(
["src/test/java/**/*.java"],
- exclude = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS,
+ exclude = TESTUTIL + CUSTOM_TRUTH_SUBJECTS + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS,
),
resources = glob(["src/test/resources/com/google/gerrit/server/**/*"]),
visibility = ["//visibility:public"],
deps = TESTUTIL_DEPS + [
+ ":custom-truth-subjects",
":testutil",
"//gerrit-antlr:query_exception",
"//gerrit-patch-jgit:server",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java
index 9773869..880ba24 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java
@@ -79,20 +79,13 @@
* @param value only value of the metric.
* @param desc description of the metric.
*/
- public <V> void newConstantMetric(String name, final V value, Description desc) {
+ public <V> void newConstantMetric(String name, V value, Description desc) {
desc.setConstant();
@SuppressWarnings("unchecked")
Class<V> type = (Class<V>) value.getClass();
- final CallbackMetric0<V> metric = newCallbackMetric(name, type, desc);
- newTrigger(
- metric,
- new Runnable() {
- @Override
- public void run() {
- metric.set(value);
- }
- });
+ CallbackMetric0<V> metric = newCallbackMetric(name, type, desc);
+ newTrigger(metric, () -> metric.set(value));
}
/**
@@ -116,16 +109,9 @@
* @param trigger function to compute the value of the metric.
*/
public <V> void newCallbackMetric(
- String name, Class<V> valueClass, Description desc, final Supplier<V> trigger) {
- final CallbackMetric0<V> metric = newCallbackMetric(name, valueClass, desc);
- newTrigger(
- metric,
- new Runnable() {
- @Override
- public void run() {
- metric.set(trigger.get());
- }
- });
+ String name, Class<V> valueClass, Description desc, Supplier<V> trigger) {
+ CallbackMetric0<V> metric = newCallbackMetric(name, valueClass, desc);
+ newTrigger(metric, () -> metric.set(trigger.get()));
}
/**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/ProcMetricModule.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
index 11f8e50..8978e99 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
@@ -103,7 +103,7 @@
}
private void procJvmMemory(MetricMaker metrics) {
- final CallbackMetric0<Long> heapCommitted =
+ CallbackMetric0<Long> heapCommitted =
metrics.newCallbackMetric(
"proc/jvm/memory/heap_committed",
Long.class,
@@ -111,7 +111,7 @@
.setGauge()
.setUnit(Units.BYTES));
- final CallbackMetric0<Long> heapUsed =
+ CallbackMetric0<Long> heapUsed =
metrics.newCallbackMetric(
"proc/jvm/memory/heap_used",
Long.class,
@@ -119,7 +119,7 @@
.setGauge()
.setUnit(Units.BYTES));
- final CallbackMetric0<Long> nonHeapCommitted =
+ CallbackMetric0<Long> nonHeapCommitted =
metrics.newCallbackMetric(
"proc/jvm/memory/non_heap_committed",
Long.class,
@@ -127,7 +127,7 @@
.setGauge()
.setUnit(Units.BYTES));
- final CallbackMetric0<Long> nonHeapUsed =
+ CallbackMetric0<Long> nonHeapUsed =
metrics.newCallbackMetric(
"proc/jvm/memory/non_heap_used",
Long.class,
@@ -135,7 +135,7 @@
.setGauge()
.setUnit(Units.BYTES));
- final CallbackMetric0<Integer> objectPendingFinalizationCount =
+ CallbackMetric0<Integer> objectPendingFinalizationCount =
metrics.newCallbackMetric(
"proc/jvm/memory/object_pending_finalization_count",
Integer.class,
@@ -143,39 +143,36 @@
.setGauge()
.setUnit("objects"));
- final MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
+ MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
metrics.newTrigger(
ImmutableSet.<CallbackMetric<?>>of(
heapCommitted, heapUsed, nonHeapCommitted, nonHeapUsed, objectPendingFinalizationCount),
- new Runnable() {
- @Override
- public void run() {
- try {
- MemoryUsage stats = memory.getHeapMemoryUsage();
- heapCommitted.set(stats.getCommitted());
- heapUsed.set(stats.getUsed());
- } catch (IllegalArgumentException e) {
- // MXBean may throw due to a bug in Java 7; ignore.
- }
-
- MemoryUsage stats = memory.getNonHeapMemoryUsage();
- nonHeapCommitted.set(stats.getCommitted());
- nonHeapUsed.set(stats.getUsed());
-
- objectPendingFinalizationCount.set(memory.getObjectPendingFinalizationCount());
+ () -> {
+ try {
+ MemoryUsage stats = memory.getHeapMemoryUsage();
+ heapCommitted.set(stats.getCommitted());
+ heapUsed.set(stats.getUsed());
+ } catch (IllegalArgumentException e) {
+ // MXBean may throw due to a bug in Java 7; ignore.
}
+
+ MemoryUsage stats = memory.getNonHeapMemoryUsage();
+ nonHeapCommitted.set(stats.getCommitted());
+ nonHeapUsed.set(stats.getUsed());
+
+ objectPendingFinalizationCount.set(memory.getObjectPendingFinalizationCount());
});
}
private void procJvmGc(MetricMaker metrics) {
- final CallbackMetric1<String, Long> gcCount =
+ CallbackMetric1<String, Long> gcCount =
metrics.newCallbackMetric(
"proc/jvm/gc/count",
Long.class,
new Description("Number of GCs").setCumulative(),
Field.ofString("gc_name", "The name of the garbage collector"));
- final CallbackMetric1<String, Long> gcTime =
+ CallbackMetric1<String, Long> gcTime =
metrics.newCallbackMetric(
"proc/jvm/gc/time",
Long.class,
@@ -187,34 +184,26 @@
metrics.newTrigger(
gcCount,
gcTime,
- new Runnable() {
- @Override
- public void run() {
- for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
- long count = gc.getCollectionCount();
- if (count != -1) {
- gcCount.set(gc.getName(), count);
- }
- long time = gc.getCollectionTime();
- if (time != -1) {
- gcTime.set(gc.getName(), time);
- }
+ () -> {
+ for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
+ long count = gc.getCollectionCount();
+ if (count != -1) {
+ gcCount.set(gc.getName(), count);
+ }
+ long time = gc.getCollectionTime();
+ if (time != -1) {
+ gcTime.set(gc.getName(), time);
}
}
});
}
private void procJvmThread(MetricMaker metrics) {
- final ThreadMXBean thread = ManagementFactory.getThreadMXBean();
+ ThreadMXBean thread = ManagementFactory.getThreadMXBean();
metrics.newCallbackMetric(
"proc/jvm/thread/num_live",
Integer.class,
new Description("Current live thread count").setGauge().setUnit("threads"),
- new Supplier<Integer>() {
- @Override
- public Integer get() {
- return thread.getThreadCount();
- }
- });
+ () -> thread.getThreadCount());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 34fcb52..98ec569 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -119,19 +119,13 @@
GitRepositoryManager gitMgr = env.getArgs().getGitRepositoryManager();
Change change = getChange(engine);
Project.NameKey projectKey = change.getProject();
- final Repository repo;
+ Repository repo;
try {
repo = gitMgr.openRepository(projectKey);
} catch (IOException e) {
throw new SystemException(e.getMessage());
}
- env.addToCleanup(
- new Runnable() {
- @Override
- public void run() {
- repo.close();
- }
- });
+ env.addToCleanup(repo::close);
return repo;
}
};
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index cb65ed3..032d7bf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -43,6 +43,7 @@
import java.util.TreeMap;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
/**
* Copies approvals between patch sets.
@@ -140,7 +141,8 @@
TreeMap<Integer, PatchSet> patchSets = getPatchSets(cd);
- try (Repository repo = repoManager.openRepository(project.getProject().getNameKey())) {
+ try (Repository repo = repoManager.openRepository(project.getProject().getNameKey());
+ RevWalk rw = new RevWalk(repo)) {
// Walk patch sets strictly less than current in descending order.
Collection<PatchSet> allPrior =
patchSets.descendingMap().tailMap(ps.getId().get(), false).values();
@@ -154,6 +156,7 @@
changeKindCache.getChangeKind(
project.getProject().getNameKey(),
repo,
+ rw,
ObjectId.fromString(priorPs.getRevision().get()),
ObjectId.fromString(ps.getRevision().get()));
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 910fbc2..1ef284c 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
@@ -322,7 +322,7 @@
accountId,
ps.getUploader());
if (approvals.isEmpty()) {
- return Collections.emptyList();
+ return ImmutableList.of();
}
checkApprovals(approvals, changeCtl);
List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
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 029b54d..7e1be17 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
@@ -17,8 +17,8 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.inject.servlet.RequestScoped;
import java.util.function.Consumer;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java
new file mode 100644
index 0000000..6584301
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java
@@ -0,0 +1,98 @@
+// 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.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.util.cli.CmdLineParser;
+import com.google.inject.Provider;
+
+public class DynamicOptions {
+ /**
+ * To provide additional options, bind a DynamicBean. For example:
+ *
+ * <pre>
+ * bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class)
+ * .annotatedWith(Exports.named(com.google.gerrit.sshd.commands.Query.class))
+ * .to(MyOptions.class);
+ * </pre>
+ *
+ * To define the additional options, implement this interface. For example:
+ *
+ * <pre>
+ * public class MyOptions implements DynamicOptions.DynamicBean {
+ * {@literal @}Option(name = "--verbose", aliases = {"-v"}
+ * usage = "Make the operation more talkative")
+ * public boolean verbose;
+ * }
+ * </pre>
+ *
+ * The option will be prefixed by the plugin name. In the example above, if the plugin name was
+ * my-plugin, then the --verbose option as used by the caller would be --my-plugin--verbose.
+ */
+ public interface DynamicBean {}
+
+ /**
+ * The entity which provided additional options may need a way to receive a reference to the
+ * DynamicBean it provided. To do so, the existing class should implement BeanReceiver (a setter)
+ * and then provide some way for the plugin to request its DynamicBean (a getter.) For example:
+ *
+ * <pre>
+ * public class Query extends SshCommand implements DynamicOptions.BeanReceiver {
+ * public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
+ * dynamicBeans.put(plugin, dynamicBean);
+ * }
+ *
+ * public DynamicOptions.DynamicBean getDynamicBean(String plugin) {
+ * return dynamicBeans.get(plugin);
+ * }
+ * ...
+ * }
+ * }
+ * </pre>
+ */
+ public interface BeanReceiver {
+ void setDynamicBean(String plugin, DynamicBean dynamicBean);
+ }
+
+ /**
+ * To include options from DynamicBeans, setup a DynamicMap and call this parse method. For
+ * example:
+ *
+ * <pre>
+ * DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
+ *
+ * ...
+ *
+ * protected void parseCommandLine(Object options) throws UnloggedFailure {
+ * final CmdLineParser clp = newCmdLineParser(options);
+ * DynamicOptions.parse(dynamicBeans, clp, options);
+ * ...
+ * }
+ * </pre>
+ */
+ public static void parse(DynamicMap<DynamicBean> dynamicBeans, CmdLineParser clp, Object bean) {
+ for (String plugin : dynamicBeans.plugins()) {
+ Provider<DynamicBean> provider =
+ dynamicBeans.byPlugin(plugin).get(bean.getClass().getCanonicalName());
+ if (provider != null) {
+ DynamicBean dynamicBean = provider.get();
+ clp.parseWithPrefix(plugin, dynamicBean);
+ if (bean instanceof BeanReceiver) {
+ ((BeanReceiver) bean).setDynamicBean(plugin, dynamicBean);
+ }
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerByEmailSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerByEmailSet.java
new file mode 100644
index 0000000..c16c9c8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerByEmailSet.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.Table;
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import java.sql.Timestamp;
+
+/**
+ * Set of reviewers on a change that do not have a Gerrit account and were added by email instead.
+ *
+ * <p>A given account may appear in multiple states and at different timestamps. No reviewers with
+ * state {@link ReviewerStateInternal#REMOVED} are ever exposed by this interface.
+ */
+public class ReviewerByEmailSet {
+ private static final ReviewerByEmailSet EMPTY = new ReviewerByEmailSet(ImmutableTable.of());
+
+ public static ReviewerByEmailSet fromTable(
+ Table<ReviewerStateInternal, Address, Timestamp> table) {
+ return new ReviewerByEmailSet(table);
+ }
+
+ public static ReviewerByEmailSet empty() {
+ return EMPTY;
+ }
+
+ private final ImmutableTable<ReviewerStateInternal, Address, Timestamp> table;
+ private ImmutableSet<Address> users;
+
+ private ReviewerByEmailSet(Table<ReviewerStateInternal, Address, Timestamp> table) {
+ this.table = ImmutableTable.copyOf(table);
+ }
+
+ public ImmutableSet<Address> all() {
+ if (users == null) {
+ // Idempotent and immutable, don't bother locking.
+ users = ImmutableSet.copyOf(table.columnKeySet());
+ }
+ return users;
+ }
+
+ public ImmutableSet<Address> byState(ReviewerStateInternal state) {
+ return table.row(state).keySet();
+ }
+
+ public ImmutableTable<ReviewerStateInternal, Address, Timestamp> asTable() {
+ return table;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return (o instanceof ReviewerByEmailSet) && table.equals(((ReviewerByEmailSet) o).table);
+ }
+
+ @Override
+ public int hashCode() {
+ return table.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + table;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
index 0f9ec8d..e61736d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
@@ -18,6 +18,7 @@
import com.google.common.collect.Sets;
import com.google.gerrit.extensions.client.AccountFieldName;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.mail.send.EmailSender;
import com.google.inject.Inject;
import java.util.Collection;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index d45ecd8..950eac7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -14,13 +14,15 @@
package com.google.gerrit.server.account;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
-import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Streams;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Module;
@@ -29,7 +31,6 @@
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import java.util.Collections;
-import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import org.slf4j.Logger;
@@ -78,32 +79,23 @@
static class Loader extends CacheLoader<String, Set<Account.Id>> {
private final SchemaFactory<ReviewDb> schema;
- private final Provider<InternalAccountQuery> accountQueryProvider;
+
+ // This must be a provider to prevent a cyclic dependency within Google-internal glue code.
+ private final Provider<ExternalIds> externalIds;
@Inject
- Loader(SchemaFactory<ReviewDb> schema, Provider<InternalAccountQuery> accountQueryProvider) {
+ Loader(SchemaFactory<ReviewDb> schema, Provider<ExternalIds> externalIds) {
this.schema = schema;
- this.accountQueryProvider = accountQueryProvider;
+ this.externalIds = externalIds;
}
@Override
public Set<Account.Id> load(String email) throws Exception {
try (ReviewDb db = schema.open()) {
- Set<Account.Id> r = new HashSet<>();
- for (Account a : db.accounts().byPreferredEmail(email)) {
- r.add(a.getId());
- }
- for (AccountState accountState : accountQueryProvider.get().byEmailPrefix(email)) {
- if (accountState
- .getExternalIds()
- .stream()
- .filter(e -> email.equals(e.email()))
- .findAny()
- .isPresent()) {
- r.add(accountState.getAccount().getId());
- }
- }
- return ImmutableSet.copyOf(r);
+ return Streams.concat(
+ Streams.stream(db.accounts().byPreferredEmail(email)).map(a -> a.getId()),
+ externalIds.get().byEmail(db, email).stream().map(e -> e.accountId()))
+ .collect(toImmutableSet());
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 245a0be..b2f1bae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.account;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
@@ -27,6 +27,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.WatchConfig.NotifyType;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
+import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.index.account.AccountIndexer;
import com.google.gerrit.server.query.account.InternalAccountQuery;
@@ -149,6 +150,7 @@
private final GeneralPreferencesLoader loader;
private final LoadingCache<String, Optional<Account.Id>> byName;
private final Provider<WatchConfig.Accessor> watchConfig;
+ private final ExternalIds externalIds;
@Inject
ByIdLoader(
@@ -156,12 +158,14 @@
GroupCache groupCache,
GeneralPreferencesLoader loader,
@Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername,
- Provider<WatchConfig.Accessor> watchConfig) {
+ Provider<WatchConfig.Accessor> watchConfig,
+ ExternalIds externalIds) {
this.schema = sf;
this.groupCache = groupCache;
this.loader = loader;
this.byName = byUsername;
this.watchConfig = watchConfig;
+ this.externalIds = externalIds;
}
@Override
@@ -184,9 +188,6 @@
return missing(who);
}
- Set<ExternalId> externalIds =
- ExternalId.from(db.accountExternalIds().byAccount(who).toList());
-
Set<AccountGroup.UUID> internalGroups = new HashSet<>();
for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
final AccountGroup.Id groupId = g.getAccountGroupId();
@@ -205,7 +206,10 @@
}
return new AccountState(
- account, internalGroups, externalIds, watchConfig.get().getProjectWatches(who));
+ account,
+ internalGroups,
+ externalIds.byAccount(db, who),
+ watchConfig.get().getProjectWatches(who));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 77d28f9..944d008 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -14,8 +14,6 @@
package com.google.gerrit.server.account;
-import static java.util.stream.Collectors.toSet;
-
import com.google.common.base.Strings;
import com.google.gerrit.audit.AuditService;
import com.google.gerrit.common.TimeUtil;
@@ -29,6 +27,9 @@
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException;
@@ -60,6 +61,7 @@
private final AtomicBoolean awaitsFirstAccountCheck;
private final AuditService auditService;
private final Provider<InternalAccountQuery> accountQueryProvider;
+ private final ExternalIds externalIds;
private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
@Inject
@@ -73,6 +75,7 @@
ProjectCache projectCache,
AuditService auditService,
Provider<InternalAccountQuery> accountQueryProvider,
+ ExternalIds externalIds,
ExternalIdsUpdate.Server externalIdsUpdateFactory) {
this.schema = schema;
this.byIdCache = byIdCache;
@@ -84,6 +87,7 @@
this.awaitsFirstAccountCheck = new AtomicBoolean(true);
this.auditService = auditService;
this.accountQueryProvider = accountQueryProvider;
+ this.externalIds = externalIds;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
}
@@ -227,8 +231,7 @@
try {
db.accounts().upsert(Collections.singleton(account));
- ExternalId existingExtId =
- ExternalId.from(db.accountExternalIds().get(extId.key().asAccountExternalIdKey()));
+ ExternalId existingExtId = externalIds.get(db, extId.key());
if (existingExtId != null && !existingExtId.accountId().equals(extId.accountId())) {
// external ID is assigned to another account, do not overwrite
db.accounts().delete(Collections.singleton(account));
@@ -404,10 +407,7 @@
throws OrmException, AccountException, IOException, ConfigInvalidException {
try (ReviewDb db = schema.open()) {
Collection<ExternalId> filteredExtIdsByScheme =
- ExternalId.from(db.accountExternalIds().byAccount(to).toList())
- .stream()
- .filter(e -> e.isScheme(who.getExternalIdKey().scheme()))
- .collect(toSet());
+ externalIds.byAccount(db, to, who.getExternalIdKey().scheme());
if (!filteredExtIdsByScheme.isEmpty()
&& (filteredExtIdsByScheme.size() > 1
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
index 4b9b0fb..1eaf34f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.account;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import com.google.common.base.Function;
import com.google.common.base.Strings;
@@ -28,6 +28,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.WatchConfig.NotifyType;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
+import com.google.gerrit.server.account.externalids.ExternalId;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
new file mode 100644
index 0000000..f45ee7b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Singleton;
+
+/** Updates accounts. */
+@Singleton
+public class AccountsUpdate {
+ /**
+ * Inserts a new account.
+ *
+ * @throws OrmDuplicateKeyException if the account already exists
+ */
+ public void insert(ReviewDb db, Account account) throws OrmException {
+ db.accounts().insert(ImmutableSet.of(account));
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
index d1dd4b0..4dd9926 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
@@ -14,9 +14,11 @@
package com.google.gerrit.server.account;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_EXTERNAL;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_EXTERNAL;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
+
+import com.google.gerrit.server.account.externalids.ExternalId;
/**
* Information for {@link AccountManager#authenticate(AuthRequest)}.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthResult.java
index 4aced52..2b1bc96 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthResult.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthResult.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.account;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.externalids.ExternalId;
/** Result from {@link AccountManager#authenticate(AuthRequest)}. */
public class AuthResult {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 66d0bf9..12dee31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -212,6 +212,11 @@
return !access(permissionName).isEmpty();
}
+ /** @return true if the user has a permission rule specifying the range. */
+ public boolean hasExplicitRange(String permission) {
+ return GlobalCapability.hasRange(permission) && !access(permission).isEmpty();
+ }
+
/** The range of permitted values associated with a label permission. */
public PermissionRange getRange(String permission) {
if (GlobalCapability.hasRange(permission)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
index f60ee45..1a02ea1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
@@ -14,14 +14,16 @@
package com.google.gerrit.server.account;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
-import static java.util.stream.Collectors.toSet;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtjsonrpc.common.VoidResult;
import com.google.gwtorm.server.OrmDuplicateKeyException;
@@ -47,6 +49,7 @@
private final AccountCache accountCache;
private final SshKeyCache sshKeyCache;
+ private final ExternalIds externalIds;
private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
private final ReviewDb db;
@@ -57,12 +60,14 @@
ChangeUserName(
AccountCache accountCache,
SshKeyCache sshKeyCache,
+ ExternalIds externalIds,
ExternalIdsUpdate.Server externalIdsUpdateFactory,
@Assisted ReviewDb db,
@Assisted IdentifiedUser user,
@Nullable @Assisted String newUsername) {
this.accountCache = accountCache;
this.sshKeyCache = sshKeyCache;
+ this.externalIds = externalIds;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
this.db = db;
this.user = user;
@@ -73,11 +78,7 @@
public VoidResult call()
throws OrmException, NameAlreadyUsedException, InvalidUserNameException, IOException,
ConfigInvalidException {
- Collection<ExternalId> old =
- ExternalId.from(db.accountExternalIds().byAccount(user.getAccountId()).toList())
- .stream()
- .filter(e -> e.isScheme(SCHEME_USERNAME))
- .collect(toSet());
+ Collection<ExternalId> old = externalIds.byAccount(db, user.getAccountId(), SCHEME_USERNAME);
if (!old.isEmpty()) {
throw new IllegalStateException(USERNAME_CANNOT_BE_CHANGED);
}
@@ -100,8 +101,7 @@
} catch (OrmDuplicateKeyException dupeErr) {
// If we are using this identity, don't report the exception.
//
- ExternalId other =
- ExternalId.from(db.accountExternalIds().get(key.asAccountExternalIdKey()));
+ ExternalId other = externalIds.get(db, key);
if (other != null && other.accountId().equals(user.getAccountId())) {
return VoidResult.INSTANCE;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index 9e7e9a4d..b16733c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.account;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
import com.google.gerrit.audit.AuditService;
import com.google.gerrit.common.TimeUtil;
@@ -36,6 +36,9 @@
import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.api.accounts.AccountExternalIdCreator;
import com.google.gerrit.server.group.GroupsCollection;
import com.google.gerrit.server.index.account.AccountIndexer;
@@ -66,11 +69,13 @@
private final VersionedAuthorizedKeys.Accessor authorizedKeys;
private final SshKeyCache sshKeyCache;
private final AccountCache accountCache;
+ private final AccountsUpdate accountsUpdate;
private final AccountIndexer indexer;
private final AccountByEmailCache byEmailCache;
private final AccountLoader.Factory infoLoader;
private final DynamicSet<AccountExternalIdCreator> externalIdCreators;
private final AuditService auditService;
+ private final ExternalIds externalIds;
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
private final String username;
@@ -82,11 +87,13 @@
VersionedAuthorizedKeys.Accessor authorizedKeys,
SshKeyCache sshKeyCache,
AccountCache accountCache,
+ AccountsUpdate accountsUpdate,
AccountIndexer indexer,
AccountByEmailCache byEmailCache,
AccountLoader.Factory infoLoader,
DynamicSet<AccountExternalIdCreator> externalIdCreators,
AuditService auditService,
+ ExternalIds externalIds,
ExternalIdsUpdate.User externalIdsUpdateFactory,
@Assisted String username) {
this.db = db;
@@ -95,11 +102,13 @@
this.authorizedKeys = authorizedKeys;
this.sshKeyCache = sshKeyCache;
this.accountCache = accountCache;
+ this.accountsUpdate = accountsUpdate;
this.indexer = indexer;
this.byEmailCache = byEmailCache;
this.infoLoader = infoLoader;
this.externalIdCreators = externalIdCreators;
this.auditService = auditService;
+ this.externalIds = externalIds;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
this.username = username;
}
@@ -125,13 +134,11 @@
Account.Id id = new Account.Id(db.nextAccountId());
ExternalId extUser = ExternalId.createUsername(username, id, input.httpPassword);
- if (db.accountExternalIds().get(extUser.key().asAccountExternalIdKey()) != null) {
+ if (externalIds.get(db, extUser.key()) != null) {
throw new ResourceConflictException("username '" + username + "' already exists");
}
if (input.email != null) {
- if (db.accountExternalIds()
- .get(ExternalId.Key.create(SCHEME_MAILTO, input.email).asAccountExternalIdKey())
- != null) {
+ if (externalIds.get(db, ExternalId.Key.create(SCHEME_MAILTO, input.email)) != null) {
throw new UnprocessableEntityException("email '" + input.email + "' already exists");
}
if (!OutgoingEmailValidator.isValid(input.email)) {
@@ -158,7 +165,7 @@
} catch (OrmDuplicateKeyException duplicateKey) {
try {
externalIdsUpdate.delete(db, extUser);
- } catch (IOException | ConfigInvalidException | OrmException cleanupError) {
+ } catch (IOException | ConfigInvalidException cleanupError) {
// Ignored
}
throw new UnprocessableEntityException("email '" + input.email + "' already exists");
@@ -168,7 +175,7 @@
Account a = new Account(id, TimeUtil.nowTs());
a.setFullName(input.name);
a.setPreferredEmail(input.email);
- db.accounts().insert(Collections.singleton(a));
+ accountsUpdate.insert(db, a);
for (AccountGroup.Id groupId : groups) {
AccountGroupMember m = new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
index 794a2c1..bfdf06c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -27,6 +27,8 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.DeleteEmail.Input;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -43,17 +45,20 @@
private final Realm realm;
private final Provider<ReviewDb> dbProvider;
private final AccountManager accountManager;
+ private final ExternalIds externalIds;
@Inject
DeleteEmail(
Provider<CurrentUser> self,
Realm realm,
Provider<ReviewDb> dbProvider,
- AccountManager accountManager) {
+ AccountManager accountManager,
+ ExternalIds externalIds) {
this.self = self;
this.realm = realm;
this.dbProvider = dbProvider;
this.accountManager = accountManager;
+ this.externalIds = externalIds;
}
@Override
@@ -74,13 +79,9 @@
}
Set<ExternalId> extIds =
- dbProvider
- .get()
- .accountExternalIds()
- .byAccount(user.getAccountId())
- .toList()
+ externalIds
+ .byAccount(dbProvider.get(), user.getAccountId())
.stream()
- .map(ExternalId::from)
.filter(e -> email.equals(e.email()))
.collect(toSet());
if (extIds.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java
index 42726dc..27a0038 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.account;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import static java.util.stream.Collectors.toMap;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -24,9 +24,10 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -37,51 +38,43 @@
import org.eclipse.jgit.errors.ConfigInvalidException;
public class DeleteExternalIds implements RestModifyView<AccountResource, List<String>> {
- private final AccountByEmailCache accountByEmailCache;
- private final AccountCache accountCache;
- private final ExternalIdsUpdate.User externalIdsUpdateFactory;
+ private final AccountManager accountManager;
+ private final ExternalIds externalIds;
private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider;
@Inject
DeleteExternalIds(
- AccountByEmailCache accountByEmailCache,
- AccountCache accountCache,
- ExternalIdsUpdate.User externalIdsUpdateFactory,
+ AccountManager accountManager,
+ ExternalIds externalIds,
Provider<CurrentUser> self,
Provider<ReviewDb> dbProvider) {
- this.accountByEmailCache = accountByEmailCache;
- this.accountCache = accountCache;
- this.externalIdsUpdateFactory = externalIdsUpdateFactory;
+ this.accountManager = accountManager;
+ this.externalIds = externalIds;
this.self = self;
this.dbProvider = dbProvider;
}
@Override
- public Response<?> apply(AccountResource resource, List<String> externalIds)
+ public Response<?> apply(AccountResource resource, List<String> extIds)
throws RestApiException, IOException, OrmException, ConfigInvalidException {
if (self.get() != resource.getUser()) {
throw new AuthException("not allowed to delete external IDs");
}
- if (externalIds == null || externalIds.size() == 0) {
+ if (extIds == null || extIds.size() == 0) {
throw new BadRequestException("external IDs are required");
}
- Account.Id accountId = resource.getUser().getAccountId();
Map<ExternalId.Key, ExternalId> externalIdMap =
- dbProvider
- .get()
- .accountExternalIds()
- .byAccount(resource.getUser().getAccountId())
- .toList()
+ externalIds
+ .byAccount(dbProvider.get(), resource.getUser().getAccountId())
.stream()
- .map(ExternalId::from)
.collect(toMap(i -> i.key(), i -> i));
List<ExternalId> toDelete = new ArrayList<>();
ExternalId.Key last = resource.getUser().getLastLoginExternalIdKey();
- for (String externalIdStr : externalIds) {
+ for (String externalIdStr : extIds) {
ExternalId id = externalIdMap.get(ExternalId.Key.parse(externalIdStr));
if (id == null) {
@@ -98,12 +91,14 @@
}
}
- if (!toDelete.isEmpty()) {
- externalIdsUpdateFactory.create().delete(dbProvider.get(), toDelete);
- accountCache.evict(accountId);
- for (ExternalId e : toDelete) {
- accountByEmailCache.evict(e.email());
+ try {
+ for (ExternalId extId : toDelete) {
+ AuthRequest authRequest = new AuthRequest(extId.key());
+ authRequest.setEmailAddress(extId.email());
+ accountManager.unlink(extId.accountId(), authRequest);
}
+ } catch (AccountException e) {
+ throw new ResourceConflictException(e.getMessage());
}
return Response.none();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIds.java
deleted file mode 100644
index c937935..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIds.java
+++ /dev/null
@@ -1,105 +0,0 @@
-// Copyright (C) 2016 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.account;
-
-import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
-
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import java.io.IOException;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-/**
- * Class to read external IDs from NoteDb.
- *
- * <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called
- * refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content
- * is a git config file that contains an external ID. It has exactly one externalId subsection with
- * an accountId and optionally email and password:
- *
- * <pre>
- * [externalId "username:jdoe"]
- * accountId = 1003407
- * email = jdoe@example.com
- * password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
- * </pre>
- */
-@Singleton
-public class ExternalIds {
- public static final int MAX_NOTE_SZ = 1 << 19;
-
- public static ObjectId readRevision(Repository repo) throws IOException {
- Ref ref = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
- return ref != null ? ref.getObjectId() : ObjectId.zeroId();
- }
-
- public static NoteMap readNoteMap(RevWalk rw, ObjectId rev) throws IOException {
- if (!rev.equals(ObjectId.zeroId())) {
- return NoteMap.read(rw.getObjectReader(), rw.parseCommit(rev));
- }
- return NoteMap.newEmptyMap();
- }
-
- private final GitRepositoryManager repoManager;
- private final AllUsersName allUsersName;
-
- @Inject
- public ExternalIds(GitRepositoryManager repoManager, AllUsersName allUsersName) {
- this.repoManager = repoManager;
- this.allUsersName = allUsersName;
- }
-
- public ObjectId readRevision() throws IOException {
- try (Repository repo = repoManager.openRepository(allUsersName)) {
- return readRevision(repo);
- }
- }
-
- /** Reads and returns the specified external ID. */
- @Nullable
- public ExternalId get(ExternalId.Key key) throws IOException, ConfigInvalidException {
- try (Repository repo = repoManager.openRepository(allUsersName);
- RevWalk rw = new RevWalk(repo)) {
- ObjectId rev = readRevision(repo);
- if (rev.equals(ObjectId.zeroId())) {
- return null;
- }
-
- return parse(key, rw, rev);
- }
- }
-
- private ExternalId parse(ExternalId.Key key, RevWalk rw, ObjectId rev)
- throws IOException, ConfigInvalidException {
- NoteMap noteMap = readNoteMap(rw, rev);
- ObjectId noteId = key.sha1();
- if (!noteMap.contains(noteId)) {
- return null;
- }
-
- byte[] raw =
- rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
- return ExternalId.parse(noteId.name(), raw);
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index cd3c0c8..876807d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -85,10 +85,12 @@
CapabilityControl cc = resource.getUser().getCapabilities();
Map<String, Object> have = new LinkedHashMap<>();
for (String name : GlobalCapability.getAllNames()) {
- if (!name.equals(PRIORITY) && want(name) && cc.canPerform(name)) {
+ if (want(name)) {
if (GlobalCapability.hasRange(name)) {
- have.put(name, new Range(cc.getRange(name)));
- } else {
+ if (cc.hasExplicitRange(name)) {
+ have.put(name, new Range(cc.getRange(name)));
+ }
+ } else if (!name.equals(PRIORITY) && cc.canPerform(name)) {
have.put(name, true);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java
index 6ea911f..12de82c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.account;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
@@ -24,11 +24,14 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
+import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -36,26 +39,30 @@
@Singleton
public class GetExternalIds implements RestReadView<AccountResource> {
private final Provider<ReviewDb> db;
+ private final ExternalIds externalIds;
private final Provider<CurrentUser> self;
private final AuthConfig authConfig;
@Inject
- GetExternalIds(Provider<ReviewDb> db, Provider<CurrentUser> self, AuthConfig authConfig) {
+ GetExternalIds(
+ Provider<ReviewDb> db,
+ ExternalIds externalIds,
+ Provider<CurrentUser> self,
+ AuthConfig authConfig) {
this.db = db;
+ this.externalIds = externalIds;
this.self = self;
this.authConfig = authConfig;
}
@Override
public List<AccountExternalIdInfo> apply(AccountResource resource)
- throws RestApiException, OrmException {
+ throws RestApiException, IOException, OrmException {
if (self.get() != resource.getUser()) {
throw new AuthException("not allowed to get external IDs");
}
- Collection<ExternalId> ids =
- ExternalId.from(
- db.get().accountExternalIds().byAccount(resource.getUser().getAccountId()).toList());
+ Collection<ExternalId> ids = externalIds.byAccount(db.get(), resource.getUser().getAccountId());
if (ids.isEmpty()) {
return ImmutableList.of();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index 7791a2e..e7ff314 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -21,6 +21,7 @@
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.avatar.AvatarProvider;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
index 435671f..d8451bf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.account;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -26,6 +26,9 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.PutHttpPassword.Input;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -55,6 +58,7 @@
private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider;
private final AccountCache accountCache;
+ private final ExternalIds externalIds;
private final ExternalIdsUpdate.User externalIdsUpdate;
@Inject
@@ -62,10 +66,12 @@
Provider<CurrentUser> self,
Provider<ReviewDb> dbProvider,
AccountCache accountCache,
+ ExternalIds externalIds,
ExternalIdsUpdate.User externalIdsUpdate) {
this.self = self;
this.dbProvider = dbProvider;
this.accountCache = accountCache;
+ this.externalIds = externalIds;
this.externalIdsUpdate = externalIdsUpdate;
}
@@ -109,13 +115,8 @@
}
ExternalId extId =
- ExternalId.from(
- dbProvider
- .get()
- .accountExternalIds()
- .get(
- ExternalId.Key.create(SCHEME_USERNAME, user.getUserName())
- .asAccountExternalIdKey()));
+ externalIds.get(
+ dbProvider.get(), ExternalId.Key.create(SCHEME_USERNAME, user.getUserName()));
if (extId == null) {
throw new ResourceNotFoundException();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
new file mode 100644
index 0000000..ab212fb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account.externalids;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
+
+public class DisabledExternalIdCache implements ExternalIdCache {
+ public static Module module() {
+ return new AbstractModule() {
+
+ @Override
+ protected void configure() {
+ bind(ExternalIdCache.class).to(DisabledExternalIdCache.class);
+ }
+ };
+ }
+
+ @Override
+ public void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) {}
+
+ @Override
+ public void onUpdate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) {}
+
+ @Override
+ public void onReplace(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Account.Id accountId,
+ Collection<ExternalId> toRemove,
+ Collection<ExternalId> toAdd) {}
+
+ @Override
+ public void onReplaceByKeys(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Account.Id accountId,
+ Collection<ExternalId.Key> toRemove,
+ Collection<ExternalId> toAdd) {}
+
+ @Override
+ public void onReplaceByKeys(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Collection<ExternalId.Key> toRemove,
+ Collection<ExternalId> toAdd) {}
+
+ @Override
+ public void onReplace(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Collection<ExternalId> toRemove,
+ Collection<ExternalId> toAdd) {}
+
+ @Override
+ public void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) {}
+
+ @Override
+ public void onRemoveByKeys(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Account.Id accountId,
+ Collection<ExternalId.Key> extIdKeys) {}
+
+ @Override
+ public void onRemoveByKeys(
+ ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId.Key> extIdKeys) {}
+
+ @Override
+ public Set<ExternalId> byAccount(Account.Id accountId) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set<ExternalId> byEmail(String email) throws IOException {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalId.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
similarity index 89%
rename from gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalId.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
index cd10b7b..a1d21c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalId.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.account;
+package com.google.gerrit.server.account.externalids;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -23,11 +23,11 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.hash.Hashing;
-import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.AuthType;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.server.account.HashedPassword;
import java.io.Serializable;
import java.util.Collection;
import java.util.Set;
@@ -225,25 +225,10 @@
throw invalidConfig(noteId, String.format("Invalid external id: %s", externalIdKeyStr));
}
- String accountIdStr =
- externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY);
String email = externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, EMAIL_KEY);
String password =
externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, PASSWORD_KEY);
- if (accountIdStr == null) {
- throw invalidConfig(
- noteId,
- String.format(
- "Missing value for %s.%s.%s", EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
- }
- Integer accountId = Ints.tryParse(accountIdStr);
- if (accountId == null) {
- throw invalidConfig(
- noteId,
- String.format(
- "Value %s for %s.%s.%s is invalid, expected account ID",
- accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
- }
+ int accountId = readAccountId(noteId, externalIdConfig, externalIdKeyStr);
return new AutoValue_ExternalId(
externalIdKey,
@@ -252,6 +237,38 @@
Strings.emptyToNull(password));
}
+ private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr)
+ throws ConfigInvalidException {
+ String accountIdStr =
+ externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY);
+ if (accountIdStr == null) {
+ throw invalidConfig(
+ noteId,
+ String.format(
+ "Value for %s.%s.%s is missing, expected account ID",
+ EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
+ }
+
+ try {
+ int accountId =
+ externalIdConfig.getInt(EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY, -1);
+ if (accountId <= 0) {
+ throw invalidConfig(
+ noteId,
+ String.format(
+ "Value %s for %s.%s.%s is invalid, expected account ID",
+ accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
+ }
+ return accountId;
+ } catch (IllegalArgumentException e) {
+ throw invalidConfig(
+ noteId,
+ String.format(
+ "Value %s for %s.%s.%s is invalid, expected account ID",
+ accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
+ }
+ }
+
private static ConfigInvalidException invalidConfig(String noteId, String message) {
return new ConfigInvalidException(
String.format("Invalid external id config for note %s: %s", noteId, message));
@@ -321,7 +338,11 @@
public void writeToConfig(Config c) {
String externalIdKey = key().get();
- c.setInt(EXTERNAL_ID_SECTION, externalIdKey, ACCOUNT_ID_KEY, accountId().get());
+ // Do not use c.setInt(...) to write the account ID because c.setInt(...) persists integers
+ // that can be expressed in KiB as a unit strings, e.g. "1024000" is stored as "100k". Using
+ // c.setString(...) ensures that account IDs are human readable.
+ c.setString(
+ EXTERNAL_ID_SECTION, externalIdKey, ACCOUNT_ID_KEY, Integer.toString(accountId().get()));
if (email() != null) {
c.setString(EXTERNAL_ID_SECTION, externalIdKey, EMAIL_KEY, email());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
new file mode 100644
index 0000000..53a1cea
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
@@ -0,0 +1,100 @@
+// 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.account.externalids;
+
+import com.google.gerrit.reviewdb.client.Account;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import org.eclipse.jgit.lib.ObjectId;
+
+/** Caches external IDs of all accounts */
+interface ExternalIdCache {
+ void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId)
+ throws IOException;
+
+ void onUpdate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId)
+ throws IOException;
+
+ void onReplace(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Account.Id accountId,
+ Collection<ExternalId> toRemove,
+ Collection<ExternalId> toAdd)
+ throws IOException;
+
+ void onReplaceByKeys(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Account.Id accountId,
+ Collection<ExternalId.Key> toRemove,
+ Collection<ExternalId> toAdd)
+ throws IOException;
+
+ void onReplaceByKeys(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Collection<ExternalId.Key> toRemove,
+ Collection<ExternalId> toAdd)
+ throws IOException;
+
+ void onReplace(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Collection<ExternalId> toRemove,
+ Collection<ExternalId> toAdd)
+ throws IOException;
+
+ void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId)
+ throws IOException;
+
+ void onRemoveByKeys(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Account.Id accountId,
+ Collection<ExternalId.Key> extIdKeys)
+ throws IOException;
+
+ void onRemoveByKeys(
+ ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId.Key> extIdKeys)
+ throws IOException;
+
+ Set<ExternalId> byAccount(Account.Id accountId) throws IOException;
+
+ Set<ExternalId> byEmail(String email) throws IOException;
+
+ default void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, ExternalId extId)
+ throws IOException {
+ onCreate(oldNotesRev, newNotesRev, Collections.singleton(extId));
+ }
+
+ default void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, ExternalId extId)
+ throws IOException {
+ onRemove(oldNotesRev, newNotesRev, Collections.singleton(extId));
+ }
+
+ default void onRemoveByKey(
+ ObjectId oldNotesRev, ObjectId newNotesRev, Account.Id accountId, ExternalId.Key extIdKey)
+ throws IOException {
+ onRemoveByKeys(oldNotesRev, newNotesRev, accountId, Collections.singleton(extIdKey));
+ }
+
+ default void onUpdate(ObjectId oldNotesRev, ObjectId newNotesRev, ExternalId updatedExtId)
+ throws IOException {
+ onUpdate(oldNotesRev, newNotesRev, Collections.singleton(updatedExtId));
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
new file mode 100644
index 0000000..0624e19
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -0,0 +1,270 @@
+// 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.account.externalids;
+
+import static java.util.stream.Collectors.toSet;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
+import org.eclipse.jgit.lib.ObjectId;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Caches external IDs of all accounts. The external IDs are always loaded from NoteDb. */
+@Singleton
+class ExternalIdCacheImpl implements ExternalIdCache {
+ private static final Logger log = LoggerFactory.getLogger(ExternalIdCacheImpl.class);
+
+ public static final String CACHE_NAME = "external_ids_map";
+
+ private final LoadingCache<ObjectId, ImmutableSetMultimap<Account.Id, ExternalId>>
+ extIdsByAccount;
+ private final ExternalIdReader externalIdReader;
+ private final Lock lock;
+
+ @Inject
+ ExternalIdCacheImpl(
+ @Named(CACHE_NAME)
+ LoadingCache<ObjectId, ImmutableSetMultimap<Account.Id, ExternalId>> extIdsByAccount,
+ ExternalIdReader externalIdReader) {
+ this.extIdsByAccount = extIdsByAccount;
+ this.externalIdReader = externalIdReader;
+ this.lock = new ReentrantLock(true /* fair */);
+ }
+
+ @Override
+ public void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extIds)
+ throws IOException {
+ updateCache(
+ oldNotesRev,
+ newNotesRev,
+ m -> {
+ for (ExternalId extId : extIds) {
+ m.put(extId.accountId(), extId);
+ }
+ });
+ }
+
+ @Override
+ public void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extIds)
+ throws IOException {
+ updateCache(
+ oldNotesRev,
+ newNotesRev,
+ m -> {
+ for (ExternalId extId : extIds) {
+ m.remove(extId.accountId(), extId);
+ }
+ });
+ }
+
+ @Override
+ public void onRemoveByKeys(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Account.Id accountId,
+ Collection<ExternalId.Key> extIdKeys)
+ throws IOException {
+ updateCache(oldNotesRev, newNotesRev, m -> removeKeys(m.get(accountId), extIdKeys));
+ }
+
+ @Override
+ public void onRemoveByKeys(
+ ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId.Key> extIdKeys)
+ throws IOException {
+ updateCache(oldNotesRev, newNotesRev, m -> removeKeys(m.values(), extIdKeys));
+ }
+
+ @Override
+ public void onUpdate(
+ ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> updatedExtIds)
+ throws IOException {
+ updateCache(
+ oldNotesRev,
+ newNotesRev,
+ m -> {
+ removeKeys(m.values(), updatedExtIds.stream().map(e -> e.key()).collect(toSet()));
+ for (ExternalId updatedExtId : updatedExtIds) {
+ m.put(updatedExtId.accountId(), updatedExtId);
+ }
+ });
+ }
+
+ @Override
+ public void onReplace(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Account.Id accountId,
+ Collection<ExternalId> toRemove,
+ Collection<ExternalId> toAdd)
+ throws IOException {
+ ExternalIdsUpdate.checkSameAccount(Iterables.concat(toRemove, toAdd), accountId);
+
+ updateCache(
+ oldNotesRev,
+ newNotesRev,
+ m -> {
+ for (ExternalId extId : toRemove) {
+ m.remove(extId.accountId(), extId);
+ }
+ for (ExternalId extId : toAdd) {
+ m.put(extId.accountId(), extId);
+ }
+ });
+ }
+
+ @Override
+ public void onReplaceByKeys(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Account.Id accountId,
+ Collection<ExternalId.Key> toRemove,
+ Collection<ExternalId> toAdd)
+ throws IOException {
+ ExternalIdsUpdate.checkSameAccount(toAdd, accountId);
+
+ updateCache(
+ oldNotesRev,
+ newNotesRev,
+ m -> {
+ removeKeys(m.get(accountId), toRemove);
+ for (ExternalId extId : toAdd) {
+ m.put(extId.accountId(), extId);
+ }
+ });
+ }
+
+ @Override
+ public void onReplaceByKeys(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Collection<ExternalId.Key> toRemove,
+ Collection<ExternalId> toAdd)
+ throws IOException {
+ updateCache(
+ oldNotesRev,
+ newNotesRev,
+ m -> {
+ removeKeys(m.values(), toRemove);
+ for (ExternalId extId : toAdd) {
+ m.put(extId.accountId(), extId);
+ }
+ });
+ }
+
+ @Override
+ public void onReplace(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Collection<ExternalId> toRemove,
+ Collection<ExternalId> toAdd)
+ throws IOException {
+ updateCache(
+ oldNotesRev,
+ newNotesRev,
+ m -> {
+ for (ExternalId extId : toRemove) {
+ m.remove(extId.accountId(), extId);
+ }
+ for (ExternalId extId : toAdd) {
+ m.put(extId.accountId(), extId);
+ }
+ });
+ }
+
+ @Override
+ public Set<ExternalId> byAccount(Account.Id accountId) throws IOException {
+ try {
+ return extIdsByAccount.get(externalIdReader.readRevision()).get(accountId);
+ } catch (ExecutionException e) {
+ throw new IOException("Cannot list external ids by account", e);
+ }
+ }
+
+ @Override
+ public Set<ExternalId> byEmail(String email) throws IOException {
+ try {
+ return extIdsByAccount
+ .get(externalIdReader.readRevision())
+ .values()
+ .stream()
+ .filter(e -> email.equals(e.email()))
+ .collect(toSet());
+ } catch (ExecutionException e) {
+ throw new IOException("Cannot list external ids by email", e);
+ }
+ }
+
+ private void updateCache(
+ ObjectId oldNotesRev,
+ ObjectId newNotesRev,
+ Consumer<Multimap<Account.Id, ExternalId>> update) {
+ lock.lock();
+ try {
+ ListMultimap<Account.Id, ExternalId> m;
+ if (!ObjectId.zeroId().equals(oldNotesRev)) {
+ m = MultimapBuilder.hashKeys().arrayListValues().build(extIdsByAccount.get(oldNotesRev));
+ } else {
+ m = MultimapBuilder.hashKeys().arrayListValues().build();
+ }
+ update.accept(m);
+ extIdsByAccount.put(newNotesRev, ImmutableSetMultimap.copyOf(m));
+ } catch (ExecutionException e) {
+ log.warn("Cannot update external IDs", e);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private static void removeKeys(Collection<ExternalId> ids, Collection<ExternalId.Key> toRemove) {
+ Collections2.transform(ids, e -> e.key()).removeAll(toRemove);
+ }
+
+ static class Loader extends CacheLoader<ObjectId, ImmutableSetMultimap<Account.Id, ExternalId>> {
+ private final ExternalIdReader externalIdReader;
+
+ @Inject
+ Loader(ExternalIdReader externalIdReader) {
+ this.externalIdReader = externalIdReader;
+ }
+
+ @Override
+ public ImmutableSetMultimap<Account.Id, ExternalId> load(ObjectId notesRev) throws Exception {
+ Multimap<Account.Id, ExternalId> extIdsByAccount =
+ MultimapBuilder.hashKeys().arrayListValues().build();
+ for (ExternalId extId : externalIdReader.all(notesRev)) {
+ extIdsByAccount.put(extId.accountId(), extId);
+ }
+ return ImmutableSetMultimap.copyOf(extIdsByAccount);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
new file mode 100644
index 0000000..3486b4e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account.externalids;
+
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.account.externalids.ExternalIdCacheImpl.Loader;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.TypeLiteral;
+import org.eclipse.jgit.lib.ObjectId;
+
+public class ExternalIdModule extends CacheModule {
+ @Override
+ protected void configure() {
+ cache(
+ ExternalIdCacheImpl.CACHE_NAME,
+ ObjectId.class,
+ new TypeLiteral<ImmutableSetMultimap<Account.Id, ExternalId>>() {})
+ // The cached data is potentially pretty large and we are always only interested
+ // in the latest value, hence the maximum cache weight is set to 1.
+ // This can lead to extra cache loads in case of the following race:
+ // 1. thread 1 reads the notes ref at revision A
+ // 2. thread 2 updates the notes ref to revision B and stores the derived value
+ // for B in the cache
+ // 3. thread 1 attempts to read the data for revision A from the cache, and misses
+ // 4. later threads attempt to read at B
+ // In this race unneeded reloads are done in step 3 (reload from revision A) and
+ // step 4 (reload from revision B, because the value for revision B was lost when the
+ // reload from revision A was done, since the cache can hold only one entry).
+ // These reloads could be avoided by increasing the cache size to 2. However the race
+ // window between reading the ref and looking it up in the cache is small so that
+ // it's rare that this race happens. Therefore it's not worth to double the memory
+ // usage of this cache, just to avoid this.
+ .maximumWeight(1)
+ .loader(Loader.class);
+
+ bind(ExternalIdCacheImpl.class);
+ bind(ExternalIdCache.class).to(ExternalIdCacheImpl.class);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
new file mode 100644
index 0000000..b12e7ed
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
@@ -0,0 +1,220 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account.externalids;
+
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Description.Units;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer0;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Class to read external IDs from ReviewDb or NoteDb.
+ *
+ * <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called
+ * refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content
+ * is a git config file that contains an external ID. It has exactly one externalId subsection with
+ * an accountId and optionally email and password:
+ *
+ * <pre>
+ * [externalId "username:jdoe"]
+ * accountId = 1003407
+ * email = jdoe@example.com
+ * password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
+ * </pre>
+ */
+@Singleton
+public class ExternalIdReader {
+ private static final Logger log = LoggerFactory.getLogger(ExternalIdReader.class);
+
+ public static final int MAX_NOTE_SZ = 1 << 19;
+
+ public static ObjectId readRevision(Repository repo) throws IOException {
+ Ref ref = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
+ return ref != null ? ref.getObjectId() : ObjectId.zeroId();
+ }
+
+ public static NoteMap readNoteMap(RevWalk rw, ObjectId rev) throws IOException {
+ if (!rev.equals(ObjectId.zeroId())) {
+ return NoteMap.read(rw.getObjectReader(), rw.parseCommit(rev));
+ }
+ return NoteMap.newEmptyMap();
+ }
+
+ private final boolean readFromGit;
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsersName;
+ private boolean failOnLoad = false;
+ private final Timer0 readAllLatency;
+
+ @Inject
+ ExternalIdReader(
+ @GerritServerConfig Config cfg,
+ GitRepositoryManager repoManager,
+ AllUsersName allUsersName,
+ MetricMaker metricMaker) {
+ this.readFromGit = cfg.getBoolean("user", null, "readExternalIdsFromGit", false);
+ this.repoManager = repoManager;
+ this.allUsersName = allUsersName;
+ this.readAllLatency =
+ metricMaker.newTimer(
+ "notedb/read_all_external_ids_latency",
+ new Description("Latency for reading all external IDs from NoteDb.")
+ .setCumulative()
+ .setUnit(Units.MILLISECONDS));
+ }
+
+ @VisibleForTesting
+ public void setFailOnLoad(boolean failOnLoad) {
+ this.failOnLoad = failOnLoad;
+ }
+
+ boolean readFromGit() {
+ return readFromGit;
+ }
+
+ ObjectId readRevision() throws IOException {
+ try (Repository repo = repoManager.openRepository(allUsersName)) {
+ return readRevision(repo);
+ }
+ }
+
+ /** Reads and returns all external IDs. */
+ Set<ExternalId> all(ReviewDb db) throws IOException, OrmException {
+ checkReadEnabled();
+
+ if (readFromGit) {
+ try (Repository repo = repoManager.openRepository(allUsersName)) {
+ return all(repo, readRevision(repo));
+ }
+ }
+
+ return ExternalId.from(db.accountExternalIds().all().toList());
+ }
+
+ /**
+ * Reads and returns all external IDs from the specified revision of the refs/meta/external-ids
+ * branch.
+ */
+ Set<ExternalId> all(ObjectId rev) throws IOException {
+ checkReadEnabled();
+
+ try (Repository repo = repoManager.openRepository(allUsersName)) {
+ return all(repo, rev);
+ }
+ }
+
+ /** Reads and returns all external IDs. */
+ private Set<ExternalId> all(Repository repo, ObjectId rev) throws IOException {
+ if (rev.equals(ObjectId.zeroId())) {
+ return ImmutableSet.of();
+ }
+
+ try (Timer0.Context ctx = readAllLatency.start();
+ RevWalk rw = new RevWalk(repo)) {
+ NoteMap noteMap = readNoteMap(rw, rev);
+ Set<ExternalId> extIds = new HashSet<>();
+ for (Note note : noteMap) {
+ byte[] raw =
+ rw.getObjectReader().open(note.getData(), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
+ try {
+ extIds.add(ExternalId.parse(note.getName(), raw));
+ } catch (ConfigInvalidException e) {
+ log.error(String.format("Ignoring invalid external ID note %s", note.getName()), e);
+ }
+ }
+ return extIds;
+ }
+ }
+
+ /** Reads and returns the specified external ID. */
+ @Nullable
+ ExternalId get(ReviewDb db, ExternalId.Key key)
+ throws IOException, ConfigInvalidException, OrmException {
+ checkReadEnabled();
+
+ if (readFromGit) {
+ try (Repository repo = repoManager.openRepository(allUsersName);
+ RevWalk rw = new RevWalk(repo)) {
+ ObjectId rev = readRevision(repo);
+ if (rev.equals(ObjectId.zeroId())) {
+ return null;
+ }
+
+ return parse(key, rw, rev);
+ }
+ }
+ return ExternalId.from(db.accountExternalIds().get(key.asAccountExternalIdKey()));
+ }
+
+ /** Reads and returns the specified external ID from the given revision. */
+ @Nullable
+ ExternalId get(ExternalId.Key key, ObjectId rev) throws IOException, ConfigInvalidException {
+ checkReadEnabled();
+
+ if (rev.equals(ObjectId.zeroId())) {
+ return null;
+ }
+
+ try (Repository repo = repoManager.openRepository(allUsersName);
+ RevWalk rw = new RevWalk(repo)) {
+ return parse(key, rw, rev);
+ }
+ }
+
+ private static ExternalId parse(ExternalId.Key key, RevWalk rw, ObjectId rev)
+ throws IOException, ConfigInvalidException {
+ NoteMap noteMap = readNoteMap(rw, rev);
+ ObjectId noteId = key.sha1();
+ if (!noteMap.contains(noteId)) {
+ return null;
+ }
+
+ byte[] raw =
+ rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
+ return ExternalId.parse(noteId.name(), raw);
+ }
+
+ private void checkReadEnabled() throws IOException {
+ if (failOnLoad) {
+ throw new IOException("Reading from external IDs is disabled");
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIds.java
new file mode 100644
index 0000000..b77fed8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIds.java
@@ -0,0 +1,93 @@
+// 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.account.externalids;
+
+import static java.util.stream.Collectors.toSet;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+
+/**
+ * Class to access external IDs.
+ *
+ * <p>The external IDs are either read from NoteDb or retrieved from the cache.
+ */
+@Singleton
+public class ExternalIds {
+ private final ExternalIdReader externalIdReader;
+ private final ExternalIdCache externalIdCache;
+
+ @Inject
+ public ExternalIds(ExternalIdReader externalIdReader, ExternalIdCache externalIdCache) {
+ this.externalIdReader = externalIdReader;
+ this.externalIdCache = externalIdCache;
+ }
+
+ /** Returns all external IDs. */
+ public Set<ExternalId> all(ReviewDb db) throws IOException, OrmException {
+ return externalIdReader.all(db);
+ }
+
+ /** Returns all external IDs from the specified revision of the refs/meta/external-ids branch. */
+ public Set<ExternalId> all(ObjectId rev) throws IOException {
+ return externalIdReader.all(rev);
+ }
+
+ /** Returns the specified external ID. */
+ @Nullable
+ public ExternalId get(ReviewDb db, ExternalId.Key key)
+ throws IOException, ConfigInvalidException, OrmException {
+ return externalIdReader.get(db, key);
+ }
+
+ /** Returns the specified external ID from the given revision. */
+ @Nullable
+ public ExternalId get(ExternalId.Key key, ObjectId rev)
+ throws IOException, ConfigInvalidException {
+ return externalIdReader.get(key, rev);
+ }
+
+ /** Returns the external IDs of the specified account. */
+ public Set<ExternalId> byAccount(ReviewDb db, Account.Id accountId)
+ throws IOException, OrmException {
+ if (externalIdReader.readFromGit()) {
+ return externalIdCache.byAccount(accountId);
+ }
+
+ return ExternalId.from(db.accountExternalIds().byAccount(accountId).toList());
+ }
+
+ /** Returns the external IDs of the specified account that have the given scheme. */
+ public Set<ExternalId> byAccount(ReviewDb db, Account.Id accountId, String scheme)
+ throws IOException, OrmException {
+ return byAccount(db, accountId).stream().filter(e -> e.key().isScheme(scheme)).collect(toSet());
+ }
+
+ public Set<ExternalId> byEmail(ReviewDb db, String email) throws IOException, OrmException {
+ if (externalIdReader.readFromGit()) {
+ return externalIdCache.byEmail(email);
+ }
+
+ return ExternalId.from(db.accountExternalIds().byEmailAddress(email).toList());
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdsBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsBatchUpdate.java
similarity index 85%
rename from gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdsBatchUpdate.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsBatchUpdate.java
index 531e562..492866d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdsBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsBatchUpdate.java
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.account;
+package com.google.gerrit.server.account.externalids;
-import static com.google.gerrit.server.account.ExternalId.toAccountExternalIds;
+import static com.google.gerrit.server.account.externalids.ExternalId.toAccountExternalIds;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -46,6 +46,7 @@
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
private final PersonIdent serverIdent;
+ private final ExternalIdCache externalIdCache;
private final Set<ExternalId> toAdd = new HashSet<>();
private final Set<ExternalId> toDelete = new HashSet<>();
@@ -53,10 +54,12 @@
public ExternalIdsBatchUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
- @GerritPersonIdent PersonIdent serverIdent) {
+ @GerritPersonIdent PersonIdent serverIdent,
+ ExternalIdCache externalIdCache) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
this.serverIdent = serverIdent;
+ this.externalIdCache = externalIdCache;
}
/**
@@ -94,9 +97,9 @@
try (Repository repo = repoManager.openRepository(allUsersName);
RevWalk rw = new RevWalk(repo);
ObjectInserter ins = repo.newObjectInserter()) {
- ObjectId rev = ExternalIds.readRevision(repo);
+ ObjectId rev = ExternalIdReader.readRevision(repo);
- NoteMap noteMap = ExternalIds.readNoteMap(rw, rev);
+ NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
for (ExternalId extId : toDelete) {
ExternalIdsUpdate.remove(rw, noteMap, extId);
@@ -106,8 +109,10 @@
ExternalIdsUpdate.insert(rw, ins, noteMap, extId);
}
- ExternalIdsUpdate.commit(
- repo, rw, ins, rev, noteMap, commitMessage, serverIdent, serverIdent);
+ ObjectId newRev =
+ ExternalIdsUpdate.commit(
+ repo, rw, ins, rev, noteMap, commitMessage, serverIdent, serverIdent);
+ externalIdCache.onReplace(rev, newRev, toDelete, toAdd);
}
toAdd.clear();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
similarity index 66%
rename from gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdsUpdate.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
index a596a8e..003928f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdsUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
@@ -12,15 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.account;
+package com.google.gerrit.server.account.externalids;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.account.ExternalId.Key.toAccountExternalIdKeys;
-import static com.google.gerrit.server.account.ExternalId.toAccountExternalIds;
-import static com.google.gerrit.server.account.ExternalIds.MAX_NOTE_SZ;
-import static com.google.gerrit.server.account.ExternalIds.readNoteMap;
-import static com.google.gerrit.server.account.ExternalIds.readRevision;
+import static com.google.gerrit.server.account.externalids.ExternalId.Key.toAccountExternalIdKeys;
+import static com.google.gerrit.server.account.externalids.ExternalId.toAccountExternalIds;
+import static com.google.gerrit.server.account.externalids.ExternalIdReader.MAX_NOTE_SZ;
+import static com.google.gerrit.server.account.externalids.ExternalIdReader.readNoteMap;
+import static com.google.gerrit.server.account.externalids.ExternalIdReader.readRevision;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
@@ -37,6 +37,9 @@
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Runnables;
import com.google.gerrit.common.Nullable;
+import com.google.gerrit.metrics.Counter0;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -53,7 +56,6 @@
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
-import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -98,21 +100,31 @@
public static class Server {
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
+ private final MetricMaker metricMaker;
+ private final ExternalIds externalIds;
+ private final ExternalIdCache externalIdCache;
private final Provider<PersonIdent> serverIdent;
@Inject
public Server(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
+ MetricMaker metricMaker,
+ ExternalIds externalIds,
+ ExternalIdCache externalIdCache,
@GerritPersonIdent Provider<PersonIdent> serverIdent) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
+ this.metricMaker = metricMaker;
+ this.externalIds = externalIds;
+ this.externalIdCache = externalIdCache;
this.serverIdent = serverIdent;
}
public ExternalIdsUpdate create() {
PersonIdent i = serverIdent.get();
- return new ExternalIdsUpdate(repoManager, allUsersName, i, i);
+ return new ExternalIdsUpdate(
+ repoManager, allUsersName, metricMaker, externalIds, externalIdCache, i, i);
}
}
@@ -126,6 +138,9 @@
public static class User {
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
+ private final MetricMaker metricMaker;
+ private final ExternalIds externalIds;
+ private final ExternalIdCache externalIdCache;
private final Provider<PersonIdent> serverIdent;
private final Provider<IdentifiedUser> identifiedUser;
@@ -133,10 +148,16 @@
public User(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
+ MetricMaker metricMaker,
+ ExternalIds externalIds,
+ ExternalIdCache externalIdCache,
@GerritPersonIdent Provider<PersonIdent> serverIdent,
Provider<IdentifiedUser> identifiedUser) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
+ this.metricMaker = metricMaker;
+ this.externalIds = externalIds;
+ this.externalIdCache = externalIdCache;
this.serverIdent = serverIdent;
this.identifiedUser = identifiedUser;
}
@@ -144,7 +165,13 @@
public ExternalIdsUpdate create() {
PersonIdent i = serverIdent.get();
return new ExternalIdsUpdate(
- repoManager, allUsersName, createPersonIdent(i, identifiedUser.get()), i);
+ repoManager,
+ allUsersName,
+ metricMaker,
+ externalIds,
+ externalIdCache,
+ createPersonIdent(i, identifiedUser.get()),
+ i);
}
private PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
@@ -153,8 +180,8 @@
}
@VisibleForTesting
- public static RetryerBuilder<Void> retryerBuilder() {
- return RetryerBuilder.<Void>newBuilder()
+ public static RetryerBuilder<RefsMetaExternalIdsUpdate> retryerBuilder() {
+ return RetryerBuilder.<RefsMetaExternalIdsUpdate>newBuilder()
.retryIfException(e -> e instanceof LockFailureException)
.withWaitStrategy(
WaitStrategies.join(
@@ -163,37 +190,61 @@
.withStopStrategy(StopStrategies.stopAfterDelay(10, TimeUnit.SECONDS));
}
- private static final Retryer<Void> RETRYER = retryerBuilder().build();
+ private static final Retryer<RefsMetaExternalIdsUpdate> RETRYER = retryerBuilder().build();
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
+ private final ExternalIds externalIds;
+ private final ExternalIdCache externalIdCache;
private final PersonIdent committerIdent;
private final PersonIdent authorIdent;
private final Runnable afterReadRevision;
- private final Retryer<Void> retryer;
+ private final Retryer<RefsMetaExternalIdsUpdate> retryer;
+ private final Counter0 updateCount;
private ExternalIdsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
+ MetricMaker metricMaker,
+ ExternalIds externalIds,
+ ExternalIdCache externalIdCache,
PersonIdent committerIdent,
PersonIdent authorIdent) {
- this(repoManager, allUsersName, committerIdent, authorIdent, Runnables.doNothing(), RETRYER);
+ this(
+ repoManager,
+ allUsersName,
+ metricMaker,
+ externalIds,
+ externalIdCache,
+ committerIdent,
+ authorIdent,
+ Runnables.doNothing(),
+ RETRYER);
}
@VisibleForTesting
public ExternalIdsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
+ MetricMaker metricMaker,
+ ExternalIds externalIds,
+ ExternalIdCache externalIdCache,
PersonIdent committerIdent,
PersonIdent authorIdent,
Runnable afterReadRevision,
- Retryer<Void> retryer) {
+ Retryer<RefsMetaExternalIdsUpdate> retryer) {
this.repoManager = checkNotNull(repoManager, "repoManager");
this.allUsersName = checkNotNull(allUsersName, "allUsersName");
this.committerIdent = checkNotNull(committerIdent, "committerIdent");
+ this.externalIds = checkNotNull(externalIds, "externalIds");
+ this.externalIdCache = checkNotNull(externalIdCache, "externalIdCache");
this.authorIdent = checkNotNull(authorIdent, "authorIdent");
this.afterReadRevision = checkNotNull(afterReadRevision, "afterReadRevision");
this.retryer = checkNotNull(retryer, "retryer");
+ this.updateCount =
+ metricMaker.newCounter(
+ "notedb/external_id_update_count",
+ new Description("Total number of external ID updates.").setRate().setUnit("updates"));
}
/**
@@ -216,12 +267,14 @@
throws IOException, ConfigInvalidException, OrmException {
db.accountExternalIds().insert(toAccountExternalIds(extIds));
- updateNoteMap(
- o -> {
- for (ExternalId extId : extIds) {
- insert(o.rw(), o.ins(), o.noteMap(), extId);
- }
- });
+ RefsMetaExternalIdsUpdate u =
+ updateNoteMap(
+ o -> {
+ for (ExternalId extId : extIds) {
+ insert(o.rw(), o.ins(), o.noteMap(), extId);
+ }
+ });
+ externalIdCache.onCreate(u.oldRev(), u.newRev(), extIds);
}
/**
@@ -243,19 +296,21 @@
throws IOException, ConfigInvalidException, OrmException {
db.accountExternalIds().upsert(toAccountExternalIds(extIds));
- updateNoteMap(
- o -> {
- for (ExternalId extId : extIds) {
- upsert(o.rw(), o.ins(), o.noteMap(), extId);
- }
- });
+ RefsMetaExternalIdsUpdate u =
+ updateNoteMap(
+ o -> {
+ for (ExternalId extId : extIds) {
+ upsert(o.rw(), o.ins(), o.noteMap(), extId);
+ }
+ });
+ externalIdCache.onUpdate(u.oldRev(), u.newRev(), extIds);
}
/**
* Deletes an external ID.
*
- * <p>The deletion fails with {@link IllegalStateException} if there is an existing external ID
- * that has the same key, but otherwise doesn't match the specified external ID.
+ * @throws IllegalStateException is thrown if there is an existing external ID that has the same
+ * key, but otherwise doesn't match the specified external ID.
*/
public void delete(ReviewDb db, ExternalId extId)
throws IOException, ConfigInvalidException, OrmException {
@@ -265,27 +320,29 @@
/**
* Deletes external IDs.
*
- * <p>The deletion fails with {@link IllegalStateException} if there is an existing external ID
- * that has the same key as any of the external IDs that should be deleted, but otherwise doesn't
- * match the that external ID.
+ * @throws IllegalStateException is thrown if there is an existing external ID that has the same
+ * key as any of the external IDs that should be deleted, but otherwise doesn't match the that
+ * external ID.
*/
public void delete(ReviewDb db, Collection<ExternalId> extIds)
throws IOException, ConfigInvalidException, OrmException {
db.accountExternalIds().delete(toAccountExternalIds(extIds));
- updateNoteMap(
- o -> {
- for (ExternalId extId : extIds) {
- remove(o.rw(), o.noteMap(), extId);
- }
- });
+ RefsMetaExternalIdsUpdate u =
+ updateNoteMap(
+ o -> {
+ for (ExternalId extId : extIds) {
+ remove(o.rw(), o.noteMap(), extId);
+ }
+ });
+ externalIdCache.onRemove(u.oldRev(), u.newRev(), extIds);
}
/**
* Delete an external ID by key.
*
- * <p>The external ID is only deleted if it belongs to the specified account. If it belongs to
- * another account the deletion fails with {@link IllegalStateException}.
+ * @throws IllegalStateException is thrown if the external ID does not belong to the specified
+ * account.
*/
public void delete(ReviewDb db, Account.Id accountId, ExternalId.Key extIdKey)
throws IOException, ConfigInvalidException, OrmException {
@@ -295,25 +352,46 @@
/**
* Delete external IDs by external ID key.
*
- * <p>The external IDs are only deleted if they belongs to the specified account. If any of the
- * external IDs belongs to another account the deletion fails with {@link IllegalStateException}.
+ * @throws IllegalStateException is thrown if any of the external IDs does not belong to the
+ * specified account.
*/
public void delete(ReviewDb db, Account.Id accountId, Collection<ExternalId.Key> extIdKeys)
throws IOException, ConfigInvalidException, OrmException {
db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(extIdKeys));
- updateNoteMap(
- o -> {
- for (ExternalId.Key extIdKey : extIdKeys) {
- remove(o.rw(), o.noteMap(), accountId, extIdKey);
- }
- });
+ RefsMetaExternalIdsUpdate u =
+ updateNoteMap(
+ o -> {
+ for (ExternalId.Key extIdKey : extIdKeys) {
+ remove(o.rw(), o.noteMap(), extIdKey, accountId);
+ }
+ });
+ externalIdCache.onRemoveByKeys(u.oldRev(), u.newRev(), accountId, extIdKeys);
+ }
+
+ /**
+ * Delete external IDs by external ID key.
+ *
+ * <p>The external IDs are deleted regardless of which account they belong to.
+ */
+ public void deleteByKeys(ReviewDb db, Collection<ExternalId.Key> extIdKeys)
+ throws IOException, ConfigInvalidException, OrmException {
+ db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(extIdKeys));
+
+ RefsMetaExternalIdsUpdate u =
+ updateNoteMap(
+ o -> {
+ for (ExternalId.Key extIdKey : extIdKeys) {
+ remove(o.rw(), o.noteMap(), extIdKey, null);
+ }
+ });
+ externalIdCache.onRemoveByKeys(u.oldRev(), u.newRev(), extIdKeys);
}
/** Deletes all external IDs of the specified account. */
public void deleteAll(ReviewDb db, Account.Id accountId)
throws IOException, ConfigInvalidException, OrmException {
- delete(db, ExternalId.from(db.accountExternalIds().byAccount(accountId).toList()));
+ delete(db, externalIds.byAccount(db, accountId));
}
/**
@@ -324,8 +402,8 @@
* be added, the old external ID with that key is deleted first and then the new external ID is
* added (so the external ID for that key is replaced).
*
- * <p>If any of the specified external IDs belongs to another account the replacement fails with
- * {@link IllegalStateException}.
+ * @throws IllegalStateException is thrown if any of the specified external IDs does not belong to
+ * the specified account.
*/
public void replace(
ReviewDb db,
@@ -338,23 +416,55 @@
db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(toDelete));
db.accountExternalIds().insert(toAccountExternalIds(toAdd));
- updateNoteMap(
- o -> {
- for (ExternalId.Key extIdKey : toDelete) {
- remove(o.rw(), o.noteMap(), accountId, extIdKey);
- }
+ RefsMetaExternalIdsUpdate u =
+ updateNoteMap(
+ o -> {
+ for (ExternalId.Key extIdKey : toDelete) {
+ remove(o.rw(), o.noteMap(), extIdKey, accountId);
+ }
- for (ExternalId extId : toAdd) {
- insert(o.rw(), o.ins(), o.noteMap(), extId);
- }
- });
+ for (ExternalId extId : toAdd) {
+ insert(o.rw(), o.ins(), o.noteMap(), extId);
+ }
+ });
+ externalIdCache.onReplaceByKeys(u.oldRev(), u.newRev(), accountId, toDelete, toAdd);
+ }
+
+ /**
+ * Replaces external IDs for an account by external ID keys.
+ *
+ * <p>Deletion of external IDs is done before adding the new external IDs. This means if an
+ * external ID key is specified for deletion and an external ID with the same key is specified to
+ * be added, the old external ID with that key is deleted first and then the new external ID is
+ * added (so the external ID for that key is replaced).
+ *
+ * <p>The external IDs are replaced regardless of which account they belong to.
+ */
+ public void replaceByKeys(
+ ReviewDb db, Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
+ throws IOException, ConfigInvalidException, OrmException {
+ db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(toDelete));
+ db.accountExternalIds().insert(toAccountExternalIds(toAdd));
+
+ RefsMetaExternalIdsUpdate u =
+ updateNoteMap(
+ o -> {
+ for (ExternalId.Key extIdKey : toDelete) {
+ remove(o.rw(), o.noteMap(), extIdKey, null);
+ }
+
+ for (ExternalId extId : toAdd) {
+ insert(o.rw(), o.ins(), o.noteMap(), extId);
+ }
+ });
+ externalIdCache.onReplaceByKeys(u.oldRev(), u.newRev(), toDelete, toAdd);
}
/**
* Replaces an external ID.
*
- * <p>If the specified external IDs belongs to different accounts the replacement fails with
- * {@link IllegalStateException}.
+ * @throws IllegalStateException is thrown if the specified external IDs belong to different
+ * accounts.
*/
public void replace(ReviewDb db, ExternalId toDelete, ExternalId toAdd)
throws IOException, ConfigInvalidException, OrmException {
@@ -369,8 +479,8 @@
* added, the old external ID with that key is deleted first and then the new external ID is added
* (so the external ID for that key is replaced).
*
- * <p>If the specified external IDs belong to different accounts the replacement fails with {@link
- * IllegalStateException}.
+ * @throws IllegalStateException is thrown if the specified external IDs belong to different
+ * accounts.
*/
public void replace(ReviewDb db, Collection<ExternalId> toDelete, Collection<ExternalId> toAdd)
throws IOException, ConfigInvalidException, OrmException {
@@ -457,8 +567,8 @@
/**
* Removes an external ID from the note map.
*
- * <p>The removal fails with {@link IllegalStateException} if there is an existing external ID
- * that has the same key, but otherwise doesn't match the specified external ID.
+ * @throws IllegalStateException is thrown if there is an existing external ID that has the same
+ * key, but otherwise doesn't match the specified external ID.
*/
public static void remove(RevWalk rw, NoteMap noteMap, ExternalId extId)
throws IOException, ConfigInvalidException {
@@ -481,11 +591,11 @@
/**
* Removes an external ID from the note map by external ID key.
*
- * <p>The external ID is only deleted if it belongs to the specified account. If the external IDs
- * belongs to another account the deletion fails with {@link IllegalStateException}.
+ * @throws IllegalStateException is thrown if an expected account ID is provided and an external
+ * ID with the specified key exists, but belongs to another account.
*/
private static void remove(
- RevWalk rw, NoteMap noteMap, Account.Id accountId, ExternalId.Key extIdKey)
+ RevWalk rw, NoteMap noteMap, ExternalId.Key extIdKey, Account.Id expectedAccountId)
throws IOException, ConfigInvalidException {
ObjectId noteId = extIdKey.sha1();
if (!noteMap.contains(noteId)) {
@@ -495,22 +605,34 @@
byte[] raw =
rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
ExternalId extId = ExternalId.parse(noteId.name(), raw);
- checkState(
- accountId.equals(extId.accountId()),
- "external id %s should be removed for account %s,"
- + " but external id belongs to account %s",
- extIdKey.get(),
- accountId.get(),
- extId.accountId().get());
+ if (expectedAccountId != null) {
+ checkState(
+ expectedAccountId.equals(extId.accountId()),
+ "external id %s should be removed for account %s,"
+ + " but external id belongs to account %s",
+ extIdKey.get(),
+ expectedAccountId.get(),
+ extId.accountId().get());
+ }
noteMap.remove(noteId);
}
- private void updateNoteMap(MyConsumer<OpenRepo> update)
+ private RefsMetaExternalIdsUpdate updateNoteMap(MyConsumer<OpenRepo> update)
throws IOException, ConfigInvalidException, OrmException {
try (Repository repo = repoManager.openRepository(allUsersName);
RevWalk rw = new RevWalk(repo);
ObjectInserter ins = repo.newObjectInserter()) {
- retryer.call(new TryNoteMapUpdate(repo, rw, ins, update));
+ return retryer.call(
+ () -> {
+ ObjectId rev = readRevision(repo);
+
+ afterReadRevision.run();
+
+ NoteMap noteMap = readNoteMap(rw, rev);
+ update.accept(OpenRepo.create(repo, rw, ins, noteMap));
+
+ return commit(repo, rw, ins, rev, noteMap);
+ });
} catch (ExecutionException | RetryException e) {
if (e.getCause() != null) {
Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
@@ -521,14 +643,16 @@
}
}
- private void commit(
+ private RefsMetaExternalIdsUpdate commit(
Repository repo, RevWalk rw, ObjectInserter ins, ObjectId rev, NoteMap noteMap)
throws IOException {
- commit(repo, rw, ins, rev, noteMap, COMMIT_MSG, committerIdent, authorIdent);
+ ObjectId newRev = commit(repo, rw, ins, rev, noteMap, COMMIT_MSG, committerIdent, authorIdent);
+ updateCount.increment();
+ return RefsMetaExternalIdsUpdate.create(rev, newRev);
}
/** Commits updates to the external IDs. */
- public static void commit(
+ public static ObjectId commit(
Repository repo,
RevWalk rw,
ObjectInserter ins,
@@ -581,12 +705,14 @@
default:
throw new IOException("Updating external IDs failed with " + res);
}
+ return rw.parseCommit(commitId);
}
private static ObjectId emptyTree(ObjectInserter ins) throws IOException {
return ins.insert(OBJ_TREE, new byte[] {});
}
+ @FunctionalInterface
private static interface MyConsumer<T> {
void accept(T t) throws IOException, ConfigInvalidException, OrmException;
}
@@ -606,31 +732,15 @@
abstract NoteMap noteMap();
}
- private class TryNoteMapUpdate implements Callable<Void> {
- private final Repository repo;
- private final RevWalk rw;
- private final ObjectInserter ins;
- private final MyConsumer<OpenRepo> update;
-
- private TryNoteMapUpdate(
- Repository repo, RevWalk rw, ObjectInserter ins, MyConsumer<OpenRepo> update) {
- this.repo = repo;
- this.rw = rw;
- this.ins = ins;
- this.update = update;
+ @VisibleForTesting
+ @AutoValue
+ public abstract static class RefsMetaExternalIdsUpdate {
+ static RefsMetaExternalIdsUpdate create(ObjectId oldRev, ObjectId newRev) {
+ return new AutoValue_ExternalIdsUpdate_RefsMetaExternalIdsUpdate(oldRev, newRev);
}
- @Override
- public Void call() throws Exception {
- ObjectId rev = readRevision(repo);
+ abstract ObjectId oldRev();
- afterReadRevision.run();
-
- NoteMap noteMap = readNoteMap(rw, rev);
- update.accept(OpenRepo.create(repo, rw, ins, noteMap));
-
- commit(repo, rw, ins, rev, noteMap);
- return null;
- }
+ abstract ObjectId newRev();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 430b6b7..23c6537 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -485,7 +485,7 @@
public List<AccountExternalIdInfo> getExternalIds() throws RestApiException {
try {
return getExternalIds.apply(account);
- } catch (OrmException e) {
+ } catch (IOException | OrmException e) {
throw new RestApiException("Cannot get external IDs", e);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java
index 2d90853..2f8dee6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java
@@ -15,7 +15,7 @@
package com.google.gerrit.server.api.accounts;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import java.util.List;
public interface AccountExternalIdCreator {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index a0babe1..4698a80 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -51,6 +51,7 @@
import com.google.gerrit.server.change.CreateMergePatchSet;
import com.google.gerrit.server.change.DeleteAssignee;
import com.google.gerrit.server.change.DeleteChange;
+import com.google.gerrit.server.change.DeletePrivate;
import com.google.gerrit.server.change.GetAssignee;
import com.google.gerrit.server.change.GetHashtags;
import com.google.gerrit.server.change.GetPastAssignees;
@@ -64,6 +65,7 @@
import com.google.gerrit.server.change.PostReviewers;
import com.google.gerrit.server.change.PublishDraftPatchSet;
import com.google.gerrit.server.change.PutAssignee;
+import com.google.gerrit.server.change.PutPrivate;
import com.google.gerrit.server.change.PutTopic;
import com.google.gerrit.server.change.Rebase;
import com.google.gerrit.server.change.Restore;
@@ -72,6 +74,7 @@
import com.google.gerrit.server.change.Revisions;
import com.google.gerrit.server.change.SubmittedTogether;
import com.google.gerrit.server.change.SuggestChangeReviewers;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.update.UpdateException;
import com.google.gwtorm.server.OrmException;
@@ -122,6 +125,8 @@
private final Check check;
private final Index index;
private final Move move;
+ private final PutPrivate putPrivate;
+ private final DeletePrivate deletePrivate;
@Inject
ChangeApiImpl(
@@ -157,6 +162,8 @@
Check check,
Index index,
Move move,
+ PutPrivate putPrivate,
+ DeletePrivate deletePrivate,
@Assisted ChangeResource change) {
this.changeApi = changeApi;
this.revert = revert;
@@ -190,6 +197,8 @@
this.check = check;
this.index = index;
this.move = move;
+ this.putPrivate = putPrivate;
+ this.deletePrivate = deletePrivate;
this.change = change;
}
@@ -235,7 +244,7 @@
public void abandon(AbandonInput in) throws RestApiException {
try {
abandon.apply(change, in);
- } catch (OrmException | UpdateException e) {
+ } catch (OrmException | UpdateException | PermissionBackendException e) {
throw new RestApiException("Cannot abandon change", e);
}
}
@@ -249,7 +258,7 @@
public void restore(RestoreInput in) throws RestApiException {
try {
restore.apply(change, in);
- } catch (OrmException | UpdateException e) {
+ } catch (OrmException | UpdateException | PermissionBackendException e) {
throw new RestApiException("Cannot restore change", e);
}
}
@@ -271,6 +280,19 @@
}
@Override
+ public void setPrivate(boolean value) throws RestApiException {
+ try {
+ if (value) {
+ putPrivate.apply(change, null);
+ } else {
+ deletePrivate.apply(change, null);
+ }
+ } catch (UpdateException e) {
+ throw new RestApiException("Cannot change private status", e);
+ }
+ }
+
+ @Override
public ChangeApi revert() throws RestApiException {
return revert(new RevertInput());
}
@@ -340,7 +362,11 @@
public void rebase(RebaseInput in) throws RestApiException {
try {
rebase.apply(change, in);
- } catch (EmailException | OrmException | UpdateException | IOException e) {
+ } catch (EmailException
+ | OrmException
+ | UpdateException
+ | IOException
+ | PermissionBackendException e) {
throw new RestApiException("Cannot rebase change", e);
}
}
@@ -365,7 +391,7 @@
in.topic = topic;
try {
putTopic.apply(change, in);
- } catch (UpdateException e) {
+ } catch (UpdateException | PermissionBackendException e) {
throw new RestApiException("Cannot set topic", e);
}
}
@@ -454,7 +480,7 @@
public void setHashtags(HashtagsInput input) throws RestApiException {
try {
postHashtags.apply(change, input);
- } catch (UpdateException e) {
+ } catch (UpdateException | PermissionBackendException e) {
throw new RestApiException("Cannot post hashtags", e);
}
}
@@ -471,8 +497,8 @@
@Override
public AccountInfo setAssignee(AssigneeInput input) throws RestApiException {
try {
- return putAssignee.apply(change, input).value();
- } catch (UpdateException | IOException | OrmException e) {
+ return putAssignee.apply(change, input);
+ } catch (UpdateException | IOException | OrmException | PermissionBackendException e) {
throw new RestApiException("Cannot set assignee", e);
}
}
@@ -501,7 +527,7 @@
try {
Response<AccountInfo> r = deleteAssignee.apply(change, null);
return r.isNone() ? null : r.value();
- } catch (UpdateException | OrmException e) {
+ } catch (UpdateException | OrmException | PermissionBackendException e) {
throw new RestApiException("Cannot delete assignee", e);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 43be8df..2af7b90 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -33,6 +33,7 @@
import com.google.gerrit.extensions.common.ActionInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.extensions.common.MergeableInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
@@ -41,6 +42,7 @@
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.change.ApplyFix;
import com.google.gerrit.server.change.CherryPick;
import com.google.gerrit.server.change.Comments;
import com.google.gerrit.server.change.CreateDraftComment;
@@ -48,6 +50,7 @@
import com.google.gerrit.server.change.DraftComments;
import com.google.gerrit.server.change.FileResource;
import com.google.gerrit.server.change.Files;
+import com.google.gerrit.server.change.Fixes;
import com.google.gerrit.server.change.GetDescription;
import com.google.gerrit.server.change.GetMergeList;
import com.google.gerrit.server.change.GetPatch;
@@ -70,6 +73,7 @@
import com.google.gerrit.server.change.TestSubmitType;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.update.UpdateException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -109,6 +113,8 @@
private final FileApiImpl.Factory fileApi;
private final ListRevisionComments listComments;
private final ListRobotComments listRobotComments;
+ private final ApplyFix applyFix;
+ private final Fixes fixes;
private final ListRevisionDrafts listDrafts;
private final CreateDraftComment createDraft;
private final DraftComments drafts;
@@ -147,6 +153,8 @@
FileApiImpl.Factory fileApi,
ListRevisionComments listComments,
ListRobotComments listRobotComments,
+ ApplyFix applyFix,
+ Fixes fixes,
ListRevisionDrafts listDrafts,
CreateDraftComment createDraft,
DraftComments drafts,
@@ -184,6 +192,8 @@
this.listComments = listComments;
this.robotComments = robotComments;
this.listRobotComments = listRobotComments;
+ this.applyFix = applyFix;
+ this.fixes = fixes;
this.listDrafts = listDrafts;
this.createDraft = createDraft;
this.drafts = drafts;
@@ -219,7 +229,7 @@
public void submit(SubmitInput in) throws RestApiException {
try {
submit.apply(revision, in);
- } catch (OrmException | IOException e) {
+ } catch (OrmException | IOException | PermissionBackendException e) {
throw new RestApiException("Cannot submit change", e);
}
}
@@ -267,7 +277,11 @@
public ChangeApi rebase(RebaseInput in) throws RestApiException {
try {
return changes.id(rebase.apply(revision, in)._number);
- } catch (OrmException | EmailException | UpdateException | IOException e) {
+ } catch (OrmException
+ | EmailException
+ | UpdateException
+ | IOException
+ | PermissionBackendException e) {
throw new RestApiException("Cannot rebase ps", e);
}
}
@@ -427,6 +441,15 @@
}
@Override
+ public EditInfo applyFix(String fixId) throws RestApiException {
+ try {
+ return applyFix.apply(fixes.parse(revision, IdString.fromDecoded(fixId)), null).value();
+ } catch (OrmException | IOException e) {
+ throw new RestApiException("Cannot apply fix", e);
+ }
+ }
+
+ @Override
public List<CommentInfo> draftsAsList() throws RestApiException {
try {
return listDrafts.getComments(revision);
@@ -544,7 +567,7 @@
in.description = description;
try {
putDescription.apply(revision, in);
- } catch (UpdateException e) {
+ } catch (UpdateException | PermissionBackendException e) {
throw new RestApiException("Cannot set description", e);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
index 15120d2..2c1ee3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -42,8 +42,8 @@
import com.google.gerrit.server.group.PutOptions;
import com.google.gerrit.server.group.PutOwner;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@@ -73,7 +73,7 @@
private final GroupResource rsrc;
private final Index index;
- @AssistedInject
+ @Inject
GroupApiImpl(
GetGroup getGroup,
GetDetail getDetail,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java
index 925b647..1595682 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java
@@ -19,8 +19,8 @@
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.project.ChildProjectResource;
import com.google.gerrit.server.project.GetChildProject;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
public class ChildProjectApiImpl implements ChildProjectApi {
interface Factory {
@@ -30,7 +30,7 @@
private final GetChildProject getChildProject;
private final ChildProjectResource rsrc;
- @AssistedInject
+ @Inject
ChildProjectApiImpl(GetChildProject getChildProject, @Assisted ChildProjectResource rsrc) {
this.getChildProject = getChildProject;
this.rsrc = rsrc;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/CommitApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/CommitApiImpl.java
new file mode 100644
index 0000000..9e17498
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/CommitApiImpl.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.projects;
+
+import com.google.gerrit.extensions.api.changes.ChangeApi;
+import com.google.gerrit.extensions.api.changes.Changes;
+import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.api.projects.CommitApi;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.change.CherryPickCommit;
+import com.google.gerrit.server.project.CommitResource;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+
+public class CommitApiImpl implements CommitApi {
+ public interface Factory {
+ CommitApiImpl create(CommitResource r);
+ }
+
+ private final Changes changes;
+ private final CherryPickCommit cherryPickCommit;
+ private final CommitResource commitResource;
+
+ @Inject
+ CommitApiImpl(
+ Changes changes, CherryPickCommit cherryPickCommit, @Assisted CommitResource commitResource) {
+ this.changes = changes;
+ this.cherryPickCommit = cherryPickCommit;
+ this.commitResource = commitResource;
+ }
+
+ @Override
+ public ChangeApi cherryPick(CherryPickInput input) throws RestApiException {
+ try {
+ return changes.id(cherryPickCommit.apply(commitResource, input)._number);
+ } catch (OrmException | IOException | UpdateException e) {
+ throw new RestApiException("Cannot cherry pick", e);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java
index 975e6c1..a4fe39b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java
@@ -26,5 +26,6 @@
factory(TagApiImpl.Factory.class);
factory(ProjectApiImpl.Factory.class);
factory(ChildProjectApiImpl.Factory.class);
+ factory(CommitApiImpl.Factory.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index e29d633..025b62a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -21,6 +21,7 @@
import com.google.gerrit.extensions.api.projects.BranchApi;
import com.google.gerrit.extensions.api.projects.BranchInfo;
import com.google.gerrit.extensions.api.projects.ChildProjectApi;
+import com.google.gerrit.extensions.api.projects.CommitApi;
import com.google.gerrit.extensions.api.projects.ConfigInfo;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.api.projects.DeleteBranchesInput;
@@ -39,6 +40,7 @@
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.project.ChildProjectsCollection;
+import com.google.gerrit.server.project.CommitsCollection;
import com.google.gerrit.server.project.CreateProject;
import com.google.gerrit.server.project.DeleteBranches;
import com.google.gerrit.server.project.DeleteTags;
@@ -89,6 +91,8 @@
private final ListTags listTags;
private final DeleteBranches deleteBranches;
private final DeleteTags deleteTags;
+ private final CommitsCollection commitsCollection;
+ private final CommitApiImpl.Factory commitApi;
@AssistedInject
ProjectApiImpl(
@@ -111,6 +115,8 @@
ListTags listTags,
DeleteBranches deleteBranches,
DeleteTags deleteTags,
+ CommitsCollection commitsCollection,
+ CommitApiImpl.Factory commitApi,
@Assisted ProjectResource project) {
this(
user,
@@ -133,6 +139,8 @@
deleteBranches,
deleteTags,
project,
+ commitsCollection,
+ commitApi,
null);
}
@@ -157,6 +165,8 @@
ListTags listTags,
DeleteBranches deleteBranches,
DeleteTags deleteTags,
+ CommitsCollection commitsCollection,
+ CommitApiImpl.Factory commitApi,
@Assisted String name) {
this(
user,
@@ -179,6 +189,8 @@
deleteBranches,
deleteTags,
null,
+ commitsCollection,
+ commitApi,
name);
}
@@ -203,6 +215,8 @@
DeleteBranches deleteBranches,
DeleteTags deleteTags,
ProjectResource project,
+ CommitsCollection commitsCollection,
+ CommitApiImpl.Factory commitApi,
String name) {
this.user = user;
this.createProjectFactory = createProjectFactory;
@@ -225,6 +239,8 @@
this.listTags = listTags;
this.deleteBranches = deleteBranches;
this.deleteTags = deleteTags;
+ this.commitsCollection = commitsCollection;
+ this.commitApi = commitApi;
}
@Override
@@ -393,6 +409,15 @@
}
}
+ @Override
+ public CommitApi commit(String commit) throws RestApiException {
+ try {
+ return commitApi.create(commitsCollection.parse(checkExists(), IdString.fromDecoded(commit)));
+ } catch (IOException e) {
+ throw new RestApiException("Cannot parse commit", e);
+ }
+ }
+
private ProjectResource checkExists() throws ResourceNotFoundException {
if (project == null) {
throw new ResourceNotFoundException(name);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index 7feb745..1a8d916 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -14,8 +14,8 @@
package com.google.gerrit.server.auth.ldap;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
import static com.google.gerrit.server.auth.ldap.Helper.LDAP_UUID;
import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_CACHE;
import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_EXIST_CACHE;
@@ -29,9 +29,9 @@
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 66b279f..9bcf3d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.auth.ldap;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
import com.google.common.base.Strings;
import com.google.common.cache.CacheLoader;
@@ -30,8 +30,9 @@
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.EmailExpander;
-import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.auth.AuthenticationUnavailableException;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -319,21 +320,19 @@
static class UserLoader extends CacheLoader<String, Optional<Account.Id>> {
private final SchemaFactory<ReviewDb> schema;
+ private final ExternalIds externalIds;
@Inject
- UserLoader(SchemaFactory<ReviewDb> schema) {
+ UserLoader(SchemaFactory<ReviewDb> schema, ExternalIds externalIds) {
this.schema = schema;
+ this.externalIds = externalIds;
}
@Override
public Optional<Account.Id> load(String username) throws Exception {
try (ReviewDb db = schema.open()) {
return Optional.ofNullable(
- ExternalId.from(
- db.accountExternalIds()
- .get(
- ExternalId.Key.create(SCHEME_GERRIT, username)
- .asAccountExternalIdKey())))
+ externalIds.get(db, ExternalId.Key.create(SCHEME_GERRIT, username)))
.map(ExternalId::accountId);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/openid/OpenIdProviderPattern.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/openid/OpenIdProviderPattern.java
index d30e667..75f4213 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/openid/OpenIdProviderPattern.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/openid/OpenIdProviderPattern.java
@@ -14,7 +14,7 @@
package com.google.gerrit.server.auth.openid;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
public class OpenIdProviderPattern {
public static OpenIdProviderPattern create(String pattern) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheMetrics.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheMetrics.java
index 862f4e8..11f2034 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheMetrics.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheMetrics.java
@@ -30,65 +30,60 @@
@Singleton
public class CacheMetrics {
@Inject
- public CacheMetrics(MetricMaker metrics, final DynamicMap<Cache<?, ?>> cacheMap) {
+ public CacheMetrics(MetricMaker metrics, DynamicMap<Cache<?, ?>> cacheMap) {
Field<String> F_NAME = Field.ofString("cache_name");
- final CallbackMetric1<String, Long> memEnt =
+ CallbackMetric1<String, Long> memEnt =
metrics.newCallbackMetric(
"caches/memory_cached",
Long.class,
new Description("Memory entries").setGauge().setUnit("entries"),
F_NAME);
- final CallbackMetric1<String, Double> memHit =
+ CallbackMetric1<String, Double> memHit =
metrics.newCallbackMetric(
"caches/memory_hit_ratio",
Double.class,
new Description("Memory hit ratio").setGauge().setUnit("percent"),
F_NAME);
- final CallbackMetric1<String, Long> memEvict =
+ CallbackMetric1<String, Long> memEvict =
metrics.newCallbackMetric(
"caches/memory_eviction_count",
Long.class,
new Description("Memory eviction count").setGauge().setUnit("evicted entries"),
F_NAME);
- final CallbackMetric1<String, Long> perDiskEnt =
+ CallbackMetric1<String, Long> perDiskEnt =
metrics.newCallbackMetric(
"caches/disk_cached",
Long.class,
new Description("Disk entries used by persistent cache").setGauge().setUnit("entries"),
F_NAME);
- final CallbackMetric1<String, Double> perDiskHit =
+ CallbackMetric1<String, Double> perDiskHit =
metrics.newCallbackMetric(
"caches/disk_hit_ratio",
Double.class,
new Description("Disk hit ratio for persistent cache").setGauge().setUnit("percent"),
F_NAME);
- final Set<CallbackMetric<?>> cacheMetrics =
+ Set<CallbackMetric<?>> cacheMetrics =
ImmutableSet.<CallbackMetric<?>>of(memEnt, memHit, memEvict, perDiskEnt, perDiskHit);
metrics.newTrigger(
cacheMetrics,
- new Runnable() {
- @Override
- public void run() {
- for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
- Cache<?, ?> c = e.getProvider().get();
- String name = metricNameOf(e);
- CacheStats cstats = c.stats();
- memEnt.set(name, c.size());
- memHit.set(name, cstats.hitRate() * 100);
- memEvict.set(name, cstats.evictionCount());
- if (c instanceof PersistentCache) {
- PersistentCache.DiskStats d = ((PersistentCache) c).diskStats();
- perDiskEnt.set(name, d.size());
- perDiskHit.set(name, hitRatio(d));
- }
- }
- for (CallbackMetric<?> cbm : cacheMetrics) {
- cbm.prune();
+ () -> {
+ for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) {
+ Cache<?, ?> c = e.getProvider().get();
+ String name = metricNameOf(e);
+ CacheStats cstats = c.stats();
+ memEnt.set(name, c.size());
+ memHit.set(name, cstats.hitRate() * 100);
+ memEvict.set(name, cstats.evictionCount());
+ if (c instanceof PersistentCache) {
+ PersistentCache.DiskStats d = ((PersistentCache) c).diskStats();
+ perDiskEnt.set(name, d.size());
+ perDiskHit.set(name, hitRatio(d));
}
}
+ cacheMetrics.forEach(CallbackMetric::prune);
});
}
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 0cafe6d..95e1f2f 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
@@ -21,7 +21,6 @@
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -32,6 +31,8 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.AbandonOp;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.UpdateException;
@@ -40,14 +41,10 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.Collection;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@Singleton
public class Abandon
implements RestModifyView<ChangeResource, AbandonInput>, UiAction<ChangeResource> {
- private static final Logger log = LoggerFactory.getLogger(Abandon.class);
-
private final Provider<ReviewDb> dbProvider;
private final ChangeJson.Factory json;
private final BatchUpdate.Factory batchUpdateFactory;
@@ -70,14 +67,15 @@
@Override
public ChangeInfo apply(ChangeResource req, AbandonInput input)
- throws RestApiException, UpdateException, OrmException {
- ChangeControl control = req.getControl();
- if (!control.canAbandon(dbProvider.get())) {
- throw new AuthException("abandon not permitted");
- }
+ throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+ req.permissions().database(dbProvider).check(ChangePermission.ABANDON);
+
Change change =
abandon(
- control, input.message, input.notify, notifyUtil.resolveAccounts(input.notifyDetails));
+ req.getControl(),
+ input.message,
+ input.notify,
+ notifyUtil.resolveAccounts(input.notifyDetails));
return json.noOptions().format(change);
}
@@ -159,19 +157,14 @@
}
@Override
- public UiAction.Description getDescription(ChangeResource resource) {
- boolean canAbandon = false;
- try {
- canAbandon = resource.getControl().canAbandon(dbProvider.get());
- } catch (OrmException e) {
- log.error("Cannot check canAbandon status. Assuming false.", e);
- }
+ public UiAction.Description getDescription(ChangeResource rsrc) {
+ Change change = rsrc.getChange();
return new UiAction.Description()
.setLabel("Abandon")
.setTitle("Abandon the change")
.setVisible(
- resource.getChange().getStatus().isOpen()
- && resource.getChange().getStatus() != Change.Status.DRAFT
- && canAbandon);
+ change.getStatus().isOpen()
+ && change.getStatus() != Change.Status.DRAFT
+ && rsrc.permissions().database(dbProvider).testOrFalse(ChangePermission.ABANDON));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
index 519a4bc..af619d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
@@ -122,6 +122,7 @@
copy.mergeable = changeInfo.mergeable;
copy.insertions = changeInfo.insertions;
copy.deletions = changeInfo.deletions;
+ copy.isPrivate = changeInfo.isPrivate;
copy.subject = changeInfo.subject;
copy.status = changeInfo.status;
copy.owner = changeInfo.owner;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java
new file mode 100644
index 0000000..77eff90
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.common.EditInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.edit.ChangeEdit;
+import com.google.gerrit.server.edit.ChangeEditJson;
+import com.google.gerrit.server.edit.ChangeEditModifier;
+import com.google.gerrit.server.edit.tree.TreeModification;
+import com.google.gerrit.server.fixes.FixReplacementInterpreter;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+@Singleton
+public class ApplyFix implements RestModifyView<FixResource, Void> {
+
+ private final GitRepositoryManager gitRepositoryManager;
+ private final FixReplacementInterpreter fixReplacementInterpreter;
+ private final ChangeEditModifier changeEditModifier;
+ private final ChangeEditJson changeEditJson;
+
+ @Inject
+ public ApplyFix(
+ GitRepositoryManager gitRepositoryManager,
+ FixReplacementInterpreter fixReplacementInterpreter,
+ ChangeEditModifier changeEditModifier,
+ ChangeEditJson changeEditJson) {
+ this.gitRepositoryManager = gitRepositoryManager;
+ this.fixReplacementInterpreter = fixReplacementInterpreter;
+ this.changeEditModifier = changeEditModifier;
+ this.changeEditJson = changeEditJson;
+ }
+
+ @Override
+ public Response<EditInfo> apply(FixResource fixResource, Void nothing)
+ throws AuthException, OrmException, ResourceConflictException, IOException,
+ ResourceNotFoundException {
+ RevisionResource revisionResource = fixResource.getRevisionResource();
+ Project.NameKey project = revisionResource.getProject();
+ ProjectState projectState = revisionResource.getControl().getProjectControl().getProjectState();
+ PatchSet patchSet = revisionResource.getPatchSet();
+ ObjectId patchSetCommitId = ObjectId.fromString(patchSet.getRevision().get());
+
+ try (Repository repository = gitRepositoryManager.openRepository(project)) {
+ List<TreeModification> treeModifications =
+ fixReplacementInterpreter.toTreeModifications(
+ repository, projectState, patchSetCommitId, fixResource.getFixReplacements());
+ ChangeEdit changeEdit =
+ changeEditModifier.combineWithModifiedPatchSetTree(
+ repository, revisionResource.getControl(), patchSet, treeModifications);
+ EditInfo editInfo = changeEditJson.toEditInfo(changeEdit, false);
+ return Response.ok(editInfo);
+ } catch (InvalidChangeOperationException e) {
+ throw new ResourceConflictException(e.getMessage());
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
index 108e180..1695d0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
@@ -16,8 +16,6 @@
import com.google.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.edit.ChangeEdit;
import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.TypeLiteral;
@@ -67,12 +65,4 @@
public String getPath() {
return path;
}
-
- Account.Id getAccountId() {
- return getUser().getAccountId();
- }
-
- IdentifiedUser getUser() {
- return edit.getUser();
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index fbb2115..332c3c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -229,7 +229,8 @@
}
try {
editInfo.files =
- fileInfoJson.toFileInfoMap(rsrc.getChange(), edit.get().getRevision(), basePatchSet);
+ fileInfoJson.toFileInfoMap(
+ rsrc.getChange(), edit.get().getEditCommit(), basePatchSet);
} catch (PatchListNotAvailableException e) {
throw new ResourceNotFoundException(e.getMessage());
}
@@ -395,7 +396,7 @@
rsrc.getControl().getProjectControl().getProjectState(),
base
? ObjectId.fromString(edit.getBasePatchSet().getRevision().get())
- : ObjectId.fromString(edit.getRevision().get()),
+ : edit.getEditCommit(),
rsrc.getPath()));
} catch (ResourceNotFoundException rnfe) {
return Response.none();
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 da34064..50a4167 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
@@ -75,6 +75,7 @@
import java.util.concurrent.Future;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.util.ChangeIdUtil;
import org.slf4j.Logger;
@@ -82,7 +83,7 @@
public class ChangeInserter implements InsertChangeOp {
public interface Factory {
- ChangeInserter create(Change.Id cid, RevCommit rc, String refName);
+ ChangeInserter create(Change.Id cid, ObjectId commitId, String refName);
}
private static final Logger log = LoggerFactory.getLogger(ChangeInserter.class);
@@ -102,7 +103,7 @@
private final Change.Id changeId;
private final PatchSet.Id psId;
- private final RevCommit commit;
+ private final ObjectId commitId;
private final String refName;
// Fields exposed as setters.
@@ -110,6 +111,7 @@
private String topic;
private String message;
private String patchSetDescription;
+ private boolean isPrivate;
private List<String> groups = Collections.emptyList();
private CommitValidators.Policy validatePolicy = CommitValidators.Policy.GERRIT;
private NotifyHandling notify = NotifyHandling.ALL;
@@ -145,7 +147,7 @@
CommentAdded commentAdded,
RevisionCreated revisionCreated,
@Assisted Change.Id changeId,
- @Assisted RevCommit commit,
+ @Assisted ObjectId commitId,
@Assisted String refName) {
this.projectControlFactory = projectControlFactory;
this.userFactory = userFactory;
@@ -162,7 +164,7 @@
this.changeId = changeId;
this.psId = new PatchSet.Id(changeId, INITIAL_PATCH_SET_ID);
- this.commit = commit;
+ this.commitId = commitId.copy();
this.refName = refName;
this.reviewers = Collections.emptySet();
this.extraCC = Collections.emptySet();
@@ -174,42 +176,45 @@
}
@Override
- public Change createChange(Context ctx) {
+ public Change createChange(Context ctx) throws IOException {
change =
new Change(
- getChangeKey(commit),
+ getChangeKey(ctx.getRevWalk(), commitId),
changeId,
ctx.getAccountId(),
new Branch.NameKey(ctx.getProject(), refName),
ctx.getWhen());
change.setStatus(MoreObjects.firstNonNull(status, Change.Status.NEW));
change.setTopic(topic);
+ change.setPrivate(isPrivate);
return change;
}
- private static Change.Key getChangeKey(RevCommit commit) {
+ private static Change.Key getChangeKey(RevWalk rw, ObjectId id) throws IOException {
+ RevCommit commit = rw.parseCommit(id);
+ rw.parseBody(commit);
List<String> idList = commit.getFooterLines(FooterConstants.CHANGE_ID);
if (!idList.isEmpty()) {
return new Change.Key(idList.get(idList.size() - 1).trim());
}
- ObjectId id =
+ ObjectId changeId =
ChangeIdUtil.computeChangeId(
commit.getTree(),
commit,
commit.getAuthorIdent(),
commit.getCommitterIdent(),
commit.getShortMessage());
- StringBuilder changeId = new StringBuilder();
- changeId.append("I").append(ObjectId.toString(id));
- return new Change.Key(changeId.toString());
+ StringBuilder changeIdStr = new StringBuilder();
+ changeIdStr.append("I").append(ObjectId.toString(changeId));
+ return new Change.Key(changeIdStr.toString());
}
public PatchSet.Id getPatchSetId() {
return psId;
}
- public RevCommit getCommit() {
- return commit;
+ public ObjectId getCommitId() {
+ return commitId;
}
public Change getChange() {
@@ -259,6 +264,12 @@
return this;
}
+ public ChangeInserter setPrivate(boolean isPrivate) {
+ checkState(change == null, "setPrivate(boolean) only valid before creating change");
+ this.isPrivate = isPrivate;
+ return this;
+ }
+
public ChangeInserter setDraft(boolean draft) {
checkState(change == null, "setDraft(boolean) only valid before creating change");
return setStatus(draft ? Change.Status.DRAFT : Change.Status.NEW);
@@ -330,7 +341,7 @@
return;
}
if (updateRefCommand == null) {
- ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), commit, psId.toRefName()));
+ ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), commitId, psId.toRefName()));
} else {
ctx.addRefUpdate(updateRefCommand);
}
@@ -342,7 +353,8 @@
change = ctx.getChange(); // Use defensive copy created by ChangeControl.
ReviewDb db = ctx.getDb();
ChangeControl ctl = ctx.getControl();
- patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId);
+ patchSetInfo =
+ patchSetInfoFactory.get(ctx.getRevWalk(), ctx.getRevWalk().parseCommit(commitId), psId);
ctx.getChange().setCurrentPatchSet(patchSetInfo);
ChangeUpdate update = ctx.getUpdate(psId);
@@ -351,11 +363,14 @@
update.setBranch(change.getDest().get());
update.setTopic(change.getTopic());
update.setPsDescription(patchSetDescription);
+ if (isPrivate) {
+ update.setPrivate(isPrivate);
+ }
boolean draft = status == Change.Status.DRAFT;
List<String> newGroups = groups;
if (newGroups.isEmpty()) {
- newGroups = GroupCollector.getDefaultGroups(commit);
+ newGroups = GroupCollector.getDefaultGroups(commitId);
}
patchSet =
psUtil.insert(
@@ -363,7 +378,7 @@
ctx.getRevWalk(),
update,
psId,
- commit,
+ commitId,
draft,
newGroups,
pushCert,
@@ -505,16 +520,18 @@
RefControl refControl =
projectControlFactory.controlFor(ctx.getProject(), ctx.getUser()).controlForRef(refName);
String refName = psId.toRefName();
- CommitReceivedEvent event =
+ try (CommitReceivedEvent event =
new CommitReceivedEvent(
- new ReceiveCommand(ObjectId.zeroId(), commit.getId(), refName),
+ new ReceiveCommand(ObjectId.zeroId(), commitId, refName),
refControl.getProjectControl().getProject(),
change.getDest().get(),
- commit,
- ctx.getIdentifiedUser());
- commitValidatorsFactory
- .create(validatePolicy, refControl, new NoSshInfo(), ctx.getRepository())
- .validate(event);
+ ctx.getRevWalk().getObjectReader(),
+ commitId,
+ ctx.getIdentifiedUser())) {
+ commitValidatorsFactory
+ .create(validatePolicy, refControl, new NoSshInfo(), ctx.getRepository())
+ .validate(event);
+ }
} catch (CommitValidationException e) {
throw new ResourceConflictException(e.getFullMessage());
} catch (NoSuchProjectException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 41d101b..2d561c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -108,6 +108,7 @@
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -122,7 +123,6 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
@@ -214,7 +214,7 @@
private AccountLoader accountLoader;
private FixInput fix;
- @AssistedInject
+ @Inject
ChangeJson(
Provider<ReviewDb> db,
LabelNormalizer ln,
@@ -439,6 +439,7 @@
info.updated = c.getLastUpdatedOn();
info._number = c.getId().get();
info.problems = result.problems();
+ info.isPrivate = c.isPrivate();
finish(info);
} else {
info = new ChangeInfo();
@@ -491,6 +492,7 @@
out.insertions = changedLines.get().insertions;
out.deletions = changedLines.get().deletions;
}
+ out.isPrivate = in.isPrivate();
out.subject = in.getSubject();
out.status = in.getStatus().asChangeStatus();
out.owner = accountLoader.get(in.getOwner());
@@ -530,6 +532,11 @@
cd.reviewers().asTable().rowMap().entrySet()) {
out.reviewers.put(e.getKey().asReviewerState(), toAccountInfo(e.getValue().keySet()));
}
+ for (Map.Entry<ReviewerStateInternal, Map<Address, Timestamp>> e :
+ cd.reviewersByEmail().asTable().rowMap().entrySet()) {
+ out.reviewers.put(
+ e.getKey().asReviewerState(), toAccountInfoByEmail(e.getValue().keySet()));
+ }
out.removableReviewers = removableReviewers(ctl, out);
}
@@ -1029,6 +1036,10 @@
cmi.message = message.getMessage();
cmi.tag = message.getTag();
cmi._revisionNumber = patchNum != null ? patchNum.get() : null;
+ Account.Id realAuthor = message.getRealAuthor();
+ if (realAuthor != null) {
+ cmi.realAuthor = accountLoader.get(realAuthor);
+ }
result.add(cmi);
}
}
@@ -1070,9 +1081,11 @@
Collection<AccountInfo> ccs = out.reviewers.get(ReviewerState.CC);
if (ccs != null) {
for (AccountInfo ai : ccs) {
- Account.Id id = new Account.Id(ai._accountId);
- if (ctl.canRemoveReviewer(id, 0)) {
- removable.add(id);
+ if (ai._accountId != null) {
+ Account.Id id = new Account.Id(ai._accountId);
+ if (ctl.canRemoveReviewer(id, 0)) {
+ removable.add(id);
+ }
}
}
}
@@ -1086,6 +1099,14 @@
for (Account.Id id : removable) {
result.add(accountLoader.get(id));
}
+ // Reviewers added by email are always removable
+ for (Collection<AccountInfo> infos : out.reviewers.values()) {
+ for (AccountInfo info : infos) {
+ if (info._accountId == null) {
+ result.add(info);
+ }
+ }
+ }
return result;
}
@@ -1097,6 +1118,14 @@
.collect(toList());
}
+ private Collection<AccountInfo> toAccountInfoByEmail(Collection<Address> addresses) {
+ return addresses
+ .stream()
+ .map(a -> new AccountInfo(a.getName(), a.getEmail()))
+ .sorted(AccountInfoComparator.ORDER_NULLS_FIRST)
+ .collect(toList());
+ }
+
@Nullable
private Repository openRepoIfNecessary(ChangeControl ctl) throws IOException {
if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) {
@@ -1105,15 +1134,21 @@
return null;
}
+ @Nullable
+ private RevWalk newRevWalk(@Nullable Repository repo) {
+ return repo != null ? new RevWalk(repo) : null;
+ }
+
private Map<String, RevisionInfo> revisions(
ChangeControl ctl, ChangeData cd, Map<PatchSet.Id, PatchSet> map, ChangeInfo changeInfo)
throws PatchListNotAvailableException, GpgException, OrmException, IOException {
Map<String, RevisionInfo> res = new LinkedHashMap<>();
- try (Repository repo = openRepoIfNecessary(ctl)) {
+ try (Repository repo = openRepoIfNecessary(ctl);
+ RevWalk rw = newRevWalk(repo)) {
for (PatchSet in : map.values()) {
if ((has(ALL_REVISIONS) || in.getId().equals(ctl.getChange().currentPatchSetId()))
&& ctl.isPatchVisible(in, db.get())) {
- res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in, repo, false, changeInfo));
+ res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in, repo, rw, false, changeInfo));
}
}
return res;
@@ -1150,9 +1185,10 @@
public RevisionInfo getRevisionInfo(ChangeControl ctl, PatchSet in)
throws PatchListNotAvailableException, GpgException, OrmException, IOException {
accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
- try (Repository repo = openRepoIfNecessary(ctl)) {
+ try (Repository repo = openRepoIfNecessary(ctl);
+ RevWalk rw = newRevWalk(repo)) {
RevisionInfo rev =
- toRevisionInfo(ctl, changeDataFactory.create(db.get(), ctl), in, repo, true, null);
+ toRevisionInfo(ctl, changeDataFactory.create(db.get(), ctl), in, repo, rw, true, null);
accountLoader.fill();
return rev;
}
@@ -1163,6 +1199,7 @@
ChangeData cd,
PatchSet in,
@Nullable Repository repo,
+ @Nullable RevWalk rw,
boolean fillCommit,
@Nullable ChangeInfo changeInfo)
throws PatchListNotAvailableException, GpgException, OrmException, IOException {
@@ -1175,32 +1212,32 @@
out.uploader = accountLoader.get(in.getUploader());
out.draft = in.isDraft() ? true : null;
out.fetch = makeFetchMap(ctl, in);
- out.kind = changeKindCache.getChangeKind(repo, cd, in);
+ out.kind = changeKindCache.getChangeKind(repo, rw, cd, in);
out.description = in.getDescription();
boolean setCommit = has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT));
boolean addFooters = out.isCurrent && has(COMMIT_FOOTERS);
if (setCommit || addFooters) {
+ checkState(rw != null);
+ checkState(repo != null);
Project.NameKey project = c.getProject();
- try (RevWalk rw = new RevWalk(repo)) {
- String rev = in.getRevision().get();
- RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
- rw.parseBody(commit);
- if (setCommit) {
- out.commit = toCommit(ctl, rw, commit, has(WEB_LINKS), fillCommit);
+ String rev = in.getRevision().get();
+ RevCommit commit = rw.parseCommit(ObjectId.fromString(rev));
+ rw.parseBody(commit);
+ if (setCommit) {
+ out.commit = toCommit(ctl, rw, commit, has(WEB_LINKS), fillCommit);
+ }
+ if (addFooters) {
+ Ref ref = repo.exactRef(ctl.getChange().getDest().get());
+ RevCommit mergeTip = null;
+ if (ref != null) {
+ mergeTip = rw.parseCommit(ref.getObjectId());
+ rw.parseBody(mergeTip);
}
- if (addFooters) {
- Ref ref = repo.exactRef(ctl.getChange().getDest().get());
- RevCommit mergeTip = null;
- if (ref != null) {
- mergeTip = rw.parseCommit(ref.getObjectId());
- rw.parseBody(mergeTip);
- }
- out.commitWithFooters =
- mergeUtilFactory
- .create(projectCache.get(project))
- .createCommitMessageOnSubmit(commit, mergeTip, ctl, in.getId());
- }
+ out.commitWithFooters =
+ mergeUtilFactory
+ .create(projectCache.get(project))
+ .createCommitMessageOnSubmit(commit, mergeTip, ctl, in.getId());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
index aa47827..e1edd62 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
@@ -23,6 +23,7 @@
import com.google.gerrit.server.query.change.ChangeData;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
/**
* Cache of {@link ChangeKind} per commit.
@@ -32,9 +33,14 @@
*/
public interface ChangeKindCache {
ChangeKind getChangeKind(
- Project.NameKey project, @Nullable Repository repo, ObjectId prior, ObjectId next);
+ Project.NameKey project,
+ @Nullable Repository repo,
+ @Nullable RevWalk rw,
+ ObjectId prior,
+ ObjectId next);
ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch);
- ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd, PatchSet patch);
+ ChangeKind getChangeKind(
+ @Nullable Repository repo, @Nullable RevWalk rw, ChangeData cd, PatchSet patch);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index 030ddd2..0e3c139 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.change;
+import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
@@ -94,10 +95,14 @@
@Override
public ChangeKind getChangeKind(
- Project.NameKey project, @Nullable Repository repo, ObjectId prior, ObjectId next) {
+ Project.NameKey project,
+ @Nullable Repository repo,
+ @Nullable RevWalk rw,
+ ObjectId prior,
+ ObjectId next) {
try {
Key key = new Key(prior, next, useRecursiveMerge);
- return new Loader(key, repoManager, project, repo).call();
+ return new Loader(key, repoManager, project, repo, rw).call();
} catch (IOException e) {
log.warn(
"Cannot check trivial rebase of new patch set " + next.name() + " in " + project, e);
@@ -111,8 +116,9 @@
}
@Override
- public ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd, PatchSet patch) {
- return getChangeKindInternal(this, repo, cd, patch);
+ public ChangeKind getChangeKind(
+ @Nullable Repository repo, @Nullable RevWalk rw, ChangeData cd, PatchSet patch) {
+ return getChangeKindInternal(this, repo, rw, cd, patch);
}
}
@@ -183,16 +189,25 @@
private final GitRepositoryManager repoManager;
private final Project.NameKey projectName;
private final Repository alreadyOpenRepo;
+ private final RevWalk alreadyOpenRw;
private Loader(
Key key,
GitRepositoryManager repoManager,
Project.NameKey projectName,
- @Nullable Repository alreadyOpenRepo) {
+ @Nullable Repository alreadyOpenRepo,
+ @Nullable RevWalk alreadyOpenRw) {
+ checkArgument(
+ (alreadyOpenRepo == null && alreadyOpenRw == null)
+ || (alreadyOpenRepo != null && alreadyOpenRw != null),
+ "must either provide both repo/revwalk, or neither; got %s/%s",
+ alreadyOpenRepo,
+ alreadyOpenRw);
this.key = key;
this.repoManager = repoManager;
this.projectName = projectName;
this.alreadyOpenRepo = alreadyOpenRepo;
+ this.alreadyOpenRw = alreadyOpenRw;
}
@Override
@@ -201,17 +216,19 @@
return ChangeKind.NO_CODE_CHANGE;
}
+ RevWalk rw = alreadyOpenRw;
Repository repo = alreadyOpenRepo;
boolean close = false;
if (repo == null) {
repo = repoManager.openRepository(projectName);
+ rw = new RevWalk(repo);
close = true;
}
- try (RevWalk walk = new RevWalk(repo)) {
- RevCommit prior = walk.parseCommit(key.prior);
- walk.parseBody(prior);
- RevCommit next = walk.parseCommit(key.next);
- walk.parseBody(next);
+ try {
+ RevCommit prior = rw.parseCommit(key.prior);
+ rw.parseBody(prior);
+ RevCommit next = rw.parseCommit(key.next);
+ rw.parseBody(next);
if (!next.getFullMessage().equals(prior.getFullMessage())) {
if (isSameDeltaAndTree(prior, next)) {
@@ -233,7 +250,7 @@
// A trivial rebase can be detected by looking for the next commit
// having the same tree as would exist when the prior commit is
// cherry-picked onto the next commit's new first parent.
- try (ObjectInserter ins = new InMemoryInserter(repo)) {
+ try (ObjectInserter ins = new InMemoryInserter(rw.getObjectReader())) {
ThreeWayMerger merger = MergeUtil.newThreeWayMerger(repo, ins, key.strategyName);
merger.setBase(prior.getParent(0));
if (merger.merge(next.getParent(0), prior)
@@ -250,6 +267,7 @@
return ChangeKind.REWORK;
} finally {
if (close) {
+ rw.close();
repo.close();
}
}
@@ -327,10 +345,14 @@
@Override
public ChangeKind getChangeKind(
- Project.NameKey project, @Nullable Repository repo, ObjectId prior, ObjectId next) {
+ Project.NameKey project,
+ @Nullable Repository repo,
+ @Nullable RevWalk rw,
+ ObjectId prior,
+ ObjectId next) {
try {
Key key = new Key(prior, next, useRecursiveMerge);
- return cache.get(key, new Loader(key, repoManager, project, repo));
+ return cache.get(key, new Loader(key, repoManager, project, repo, rw));
} catch (ExecutionException e) {
log.warn("Cannot check trivial rebase of new patch set " + next.name() + " in " + project, e);
return ChangeKind.REWORK;
@@ -343,12 +365,17 @@
}
@Override
- public ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd, PatchSet patch) {
- return getChangeKindInternal(this, repo, cd, patch);
+ public ChangeKind getChangeKind(
+ @Nullable Repository repo, @Nullable RevWalk rw, ChangeData cd, PatchSet patch) {
+ return getChangeKindInternal(this, repo, rw, cd, patch);
}
private static ChangeKind getChangeKindInternal(
- ChangeKindCache cache, @Nullable Repository repo, ChangeData change, PatchSet patch) {
+ ChangeKindCache cache,
+ @Nullable Repository repo,
+ @Nullable RevWalk rw,
+ ChangeData change,
+ PatchSet patch) {
ChangeKind kind = ChangeKind.REWORK;
// Trivial case: if we're on the first patch, we don't need to use
// the repository.
@@ -373,6 +400,7 @@
cache.getChangeKind(
change.project(),
repo,
+ rw,
ObjectId.fromString(priorPs.getRevision().get()),
ObjectId.fromString(patch.getRevision().get()));
}
@@ -401,8 +429,9 @@
// Trivial case: if we're on the first patch, we don't need to open
// the repository.
if (patch.getId().get() > 1) {
- try (Repository repo = repoManager.openRepository(change.getProject())) {
- kind = getChangeKindInternal(cache, repo, changeDataFactory.create(db, change), patch);
+ try (Repository repo = repoManager.openRepository(change.getProject());
+ RevWalk rw = new RevWalk(repo)) {
+ kind = getChangeKindInternal(cache, repo, rw, changeDataFactory.create(db, change), patch);
} catch (IOException e) {
// Do nothing; assume we have a complex change
log.warn(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
index b06f05f..0da32b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -29,12 +29,13 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectState;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import com.google.inject.TypeLiteral;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import org.eclipse.jgit.lib.ObjectId;
public class ChangeResource implements RestResource, HasETag {
@@ -53,15 +54,24 @@
ChangeResource create(ChangeControl ctl);
}
+ private final PermissionBackend permissionBackend;
private final StarredChangesUtil starredChangesUtil;
private final ChangeControl control;
- @AssistedInject
- ChangeResource(StarredChangesUtil starredChangesUtil, @Assisted ChangeControl control) {
+ @Inject
+ ChangeResource(
+ PermissionBackend permissionBackend,
+ StarredChangesUtil starredChangesUtil,
+ @Assisted ChangeControl control) {
+ this.permissionBackend = permissionBackend;
this.starredChangesUtil = starredChangesUtil;
this.control = control;
}
+ public PermissionBackend.ForChange permissions() {
+ return permissionBackend.user(getControl().getUser()).change(getNotes());
+ }
+
public ChangeControl getControl() {
return control;
}
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 e5a4d0f..18d2fc1 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
@@ -66,9 +66,7 @@
throw new BadRequestException("destination must be non-empty");
}
- @SuppressWarnings("resource")
- ReviewDb db = dbProvider.get();
- if (!control.isVisible(db)) {
+ if (!control.isVisible(dbProvider.get())) {
throw new AuthException("Cherry pick not permitted");
}
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 b2455f1..8d0da36 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
@@ -16,6 +16,7 @@
import com.google.common.base.Strings;
import com.google.gerrit.common.FooterConstants;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.restapi.MergeConflictException;
@@ -42,7 +43,6 @@
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.RefControl;
import com.google.gerrit.server.query.change.ChangeData;
@@ -59,9 +59,6 @@
import java.sql.Timestamp;
import java.util.List;
import java.util.TimeZone;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
@@ -115,23 +112,42 @@
}
public Change.Id cherryPick(
- Change change,
- PatchSet patch,
- final String message,
- final String ref,
- final RefControl refControl,
- int parent)
- throws NoSuchChangeException, OrmException, MissingObjectException,
- IncorrectObjectTypeException, IOException, InvalidChangeOperationException,
- IntegrationException, UpdateException, RestApiException {
+ Change change, PatchSet patch, String message, String ref, RefControl refControl, int parent)
+ throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
+ UpdateException, RestApiException {
+ return cherryPick(
+ change.getId(),
+ patch.getId(),
+ change.getDest(),
+ change.getTopic(),
+ change.getProject(),
+ ObjectId.fromString(patch.getRevision().get()),
+ message,
+ ref,
+ refControl,
+ parent);
+ }
- if (Strings.isNullOrEmpty(ref)) {
+ public Change.Id cherryPick(
+ @Nullable Change.Id sourceChangeId,
+ @Nullable PatchSet.Id sourcePatchId,
+ @Nullable Branch.NameKey sourceBranch,
+ @Nullable String sourceChangeTopic,
+ Project.NameKey project,
+ ObjectId sourceCommit,
+ String message,
+ String targetRef,
+ RefControl targetRefControl,
+ int parent)
+ throws OrmException, IOException, InvalidChangeOperationException, IntegrationException,
+ UpdateException, RestApiException {
+
+ if (Strings.isNullOrEmpty(targetRef)) {
throw new InvalidChangeOperationException(
"Cherry Pick: Destination branch cannot be null or empty");
}
- Project.NameKey project = change.getProject();
- String destinationBranch = RefNames.shortName(ref);
+ String destinationBranch = RefNames.shortName(targetRef);
IdentifiedUser identifiedUser = user.get();
try (Repository git = gitManager.openRepository(project);
// This inserter and revwalk *must* be passed to any BatchUpdates
@@ -140,7 +156,7 @@
ObjectInserter oi = git.newObjectInserter();
ObjectReader reader = oi.newReader();
CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader)) {
- Ref destRef = git.getRefDatabase().exactRef(ref);
+ Ref destRef = git.getRefDatabase().exactRef(targetRef);
if (destRef == null) {
throw new InvalidChangeOperationException(
String.format("Branch %s does not exist.", destinationBranch));
@@ -148,8 +164,7 @@
CodeReviewCommit mergeTip = revWalk.parseCommit(destRef.getObjectId());
- CodeReviewCommit commitToCherryPick =
- revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
+ CodeReviewCommit commitToCherryPick = revWalk.parseCommit(sourceCommit);
if (parent <= 0 || parent > commitToCherryPick.getParentCount()) {
throw new InvalidChangeOperationException(
@@ -173,7 +188,7 @@
CodeReviewCommit cherryPickCommit;
try {
- ProjectState projectState = refControl.getProjectControl().getProjectState();
+ ProjectState projectState = targetRefControl.getProjectControl().getProjectState();
cherryPickCommit =
mergeUtilFactory
.create(projectState)
@@ -197,7 +212,7 @@
changeKey = new Change.Key("I" + computedChangeId.name());
}
- Branch.NameKey newDest = new Branch.NameKey(change.getProject(), destRef.getName());
+ Branch.NameKey newDest = new Branch.NameKey(project, destRef.getName());
List<ChangeData> destChanges =
queryProvider.get().setLimit(2).byBranchKey(newDest, changeKey);
if (destChanges.size() > 1) {
@@ -207,32 +222,37 @@
+ " reside on the same branch. "
+ "Cannot create a new patch set.");
}
- try (BatchUpdate bu =
- batchUpdateFactory.create(
- db.get(), change.getDest().getParentKey(), identifiedUser, now)) {
+ try (BatchUpdate bu = batchUpdateFactory.create(db.get(), project, identifiedUser, now)) {
bu.setRepository(git, revWalk, oi);
Change.Id result;
if (destChanges.size() == 1) {
// The change key exists on the destination branch. The cherry pick
// will be added as a new patch set.
ChangeControl destCtl =
- refControl.getProjectControl().controlFor(destChanges.get(0).notes());
+ targetRefControl.getProjectControl().controlFor(destChanges.get(0).notes());
result = insertPatchSet(bu, git, destCtl, cherryPickCommit);
} else {
// Change key not found on destination branch. We can create a new
// change.
String newTopic = null;
- if (!Strings.isNullOrEmpty(change.getTopic())) {
- newTopic = change.getTopic() + "-" + newDest.getShortName();
+ if (!Strings.isNullOrEmpty(sourceChangeTopic)) {
+ newTopic = sourceChangeTopic + "-" + newDest.getShortName();
}
result =
createNewChange(
- bu, cherryPickCommit, refControl.getRefName(), newTopic, change.getDest());
+ bu,
+ cherryPickCommit,
+ targetRefControl.getRefName(),
+ newTopic,
+ sourceBranch,
+ sourceCommit);
- bu.addOp(
- change.getId(),
- new AddMessageToSourceChangeOp(
- changeMessagesUtil, patch.getId(), destinationBranch, cherryPickCommit));
+ if (sourceChangeId != null && sourcePatchId != null) {
+ bu.addOp(
+ sourceChangeId,
+ new AddMessageToSourceChangeOp(
+ changeMessagesUtil, sourcePatchId, destinationBranch, cherryPickCommit));
+ }
}
bu.execute();
return result;
@@ -240,8 +260,6 @@
} catch (MergeIdenticalTreeException | MergeConflictException e) {
throw new IntegrationException("Cherry pick failed: " + e.getMessage());
}
- } catch (RepositoryNotFoundException e) {
- throw new NoSuchChangeException(change.getId(), e);
}
}
@@ -268,8 +286,9 @@
CodeReviewCommit cherryPickCommit,
String refName,
String topic,
- Branch.NameKey sourceBranch)
- throws OrmException {
+ Branch.NameKey sourceBranch,
+ ObjectId sourceCommit)
+ throws OrmException, IOException {
Change.Id changeId = new Change.Id(seq.nextChangeId());
ChangeInserter ins =
changeInserterFactory
@@ -277,7 +296,7 @@
.setValidatePolicy(CommitValidators.Policy.GERRIT)
.setTopic(topic);
- ins.setMessage(messageForDestinationChange(ins.getPatchSetId(), sourceBranch));
+ ins.setMessage(messageForDestinationChange(ins.getPatchSetId(), sourceBranch, sourceCommit));
bu.insertChange(ins);
return changeId;
}
@@ -319,12 +338,16 @@
}
}
- private String messageForDestinationChange(PatchSet.Id patchSetId, Branch.NameKey sourceBranch) {
- return new StringBuilder("Patch Set ")
- .append(patchSetId.get())
- .append(": Cherry Picked from branch ")
- .append(sourceBranch.getShortName())
- .append(".")
- .toString();
+ private String messageForDestinationChange(
+ PatchSet.Id patchSetId, Branch.NameKey sourceBranch, ObjectId sourceCommit) {
+ StringBuilder stringBuilder = new StringBuilder("Patch Set ").append(patchSetId.get());
+
+ if (sourceBranch != null) {
+ stringBuilder.append(": Cherry Picked from branch ").append(sourceBranch.getShortName());
+ } else {
+ stringBuilder.append(": Cherry Picked from commit ").append(sourceCommit.getName());
+ }
+
+ return stringBuilder.append(".").toString();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
new file mode 100644
index 0000000..cf99d37
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.extensions.api.changes.CherryPickInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+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.RestModifyView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.IntegrationException;
+import com.google.gerrit.server.project.CommitResource;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+@Singleton
+public class CherryPickCommit implements RestModifyView<CommitResource, CherryPickInput> {
+
+ private final CherryPickChange cherryPickChange;
+ private final ChangeJson.Factory json;
+
+ @Inject
+ CherryPickCommit(CherryPickChange cherryPickChange, ChangeJson.Factory json) {
+ this.cherryPickChange = cherryPickChange;
+ this.json = json;
+ }
+
+ @Override
+ public ChangeInfo apply(CommitResource rsrc, CherryPickInput input)
+ throws OrmException, IOException, UpdateException, RestApiException {
+ String message = Strings.nullToEmpty(input.message).trim();
+ String destination = Strings.nullToEmpty(input.destination).trim();
+ int parent = input.parent == null ? 1 : input.parent;
+
+ if (destination.isEmpty()) {
+ throw new BadRequestException("destination must be non-empty");
+ }
+
+ ProjectControl projectControl = rsrc.getProject();
+ Capable capable = projectControl.canPushToAtLeastOneRef();
+ if (capable != Capable.OK) {
+ throw new AuthException(capable.getMessage());
+ }
+
+ RevCommit commit = rsrc.getCommit();
+ String refName = RefNames.fullName(destination);
+ RefControl refControl = projectControl.controlForRef(refName);
+ if (!refControl.canUpload()) {
+ throw new AuthException("Not allowed to cherry pick " + commit + " to " + destination);
+ }
+
+ Project.NameKey project = projectControl.getProject().getNameKey();
+ try {
+ Change.Id cherryPickedChangeId =
+ cherryPickChange.cherryPick(
+ null,
+ null,
+ null,
+ null,
+ project,
+ commit,
+ message.isEmpty() ? commit.getFullMessage() : message,
+ refName,
+ refControl,
+ parent);
+ return json.noOptions().format(project, cherryPickedChangeId);
+ } catch (InvalidChangeOperationException e) {
+ throw new BadRequestException(e.getMessage());
+ } catch (IntegrationException e) {
+ throw new ResourceConflictException(e.getMessage());
+ }
+ }
+}
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 5032e57..6536550f 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
@@ -122,7 +122,7 @@
commentsUtil.putComments(
ctx.getDb(), ctx.getUpdate(psId), Status.DRAFT, Collections.singleton(comment));
- ctx.bumpLastUpdatedOn(false);
+ ctx.dontBumpLastUpdatedOn();
return true;
}
}
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 b8556d6..1d5a916 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
@@ -16,7 +16,6 @@
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -30,6 +29,8 @@
import com.google.gerrit.server.change.DeleteAssignee.Input;
import com.google.gerrit.server.extensions.events.AssigneeChanged;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -69,7 +70,9 @@
@Override
public Response<AccountInfo> apply(ChangeResource rsrc, Input input)
- throws RestApiException, UpdateException, OrmException {
+ throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+ rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
+
try (BatchUpdate bu =
batchUpdateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
Op op = new Op();
@@ -88,9 +91,6 @@
@Override
public boolean updateChange(ChangeContext ctx) throws RestApiException, OrmException {
- if (!ctx.getControl().canEditAssignee()) {
- throw new AuthException("Delete Assignee not permitted");
- }
change = ctx.getChange();
ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
Account.Id currentAssigneeId = change.getAssignee();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
index 9d819b1..aee9afc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -38,12 +38,10 @@
import com.google.inject.Inject;
import java.io.IOException;
import java.util.Collection;
-import java.util.Collections;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
@@ -147,9 +145,8 @@
RevWalk revWalk = ctx.getRevWalk();
ObjectId objectId = ObjectId.fromString(patchSet.getRevision().get());
- RevCommit revCommit = revWalk.parseCommit(objectId);
- return IncludedInResolver.includedInOne(
- repository, revWalk, revCommit, Collections.singletonList(destinationRef));
+ return revWalk.isMergedInto(
+ revWalk.parseCommit(objectId), revWalk.parseCommit(destinationRef.getObjectId()));
}
private void deleteChangeElementsFromDb(ChangeContext ctx, Change.Id id) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
index 7787260..d1b26ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
@@ -101,7 +101,7 @@
Comment c = maybeComment.get();
setCommentRevId(c, patchListCache, ctx.getChange(), ps);
commentsUtil.deleteComments(ctx.getDb(), ctx.getUpdate(psId), Collections.singleton(c));
- ctx.bumpLastUpdatedOn(false);
+ ctx.dontBumpLastUpdatedOn();
return true;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
new file mode 100644
index 0000000..b91c7aa
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
@@ -0,0 +1,78 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class DeletePrivate
+ implements RestModifyView<ChangeResource, DeletePrivate.Input>, UiAction<ChangeResource> {
+ public static class Input {}
+
+ private final Provider<ReviewDb> dbProvider;
+ private final BatchUpdate.Factory batchUpdateFactory;
+
+ @Inject
+ DeletePrivate(Provider<ReviewDb> dbProvider, BatchUpdate.Factory batchUpdateFactory) {
+ this.dbProvider = dbProvider;
+ this.batchUpdateFactory = batchUpdateFactory;
+ }
+
+ @Override
+ public Response<String> apply(ChangeResource rsrc, DeletePrivate.Input input)
+ throws RestApiException, UpdateException {
+ if (!rsrc.getControl().isOwner()) {
+ throw new AuthException("not allowed to unmark private");
+ }
+
+ if (!rsrc.getChange().isPrivate()) {
+ throw new ResourceConflictException("change is not private");
+ }
+
+ ChangeControl control = rsrc.getControl();
+ SetPrivateOp op = new SetPrivateOp(false);
+ try (BatchUpdate u =
+ batchUpdateFactory.create(
+ dbProvider.get(),
+ control.getProject().getNameKey(),
+ control.getUser(),
+ TimeUtil.nowTs())) {
+ u.addOp(control.getId(), op).execute();
+ }
+
+ return Response.none();
+ }
+
+ @Override
+ public Description getDescription(ChangeResource rsrc) {
+ return new UiAction.Description()
+ .setLabel("Unmark private")
+ .setTitle("Unmark change as private")
+ .setVisible(rsrc.getControl().isOwner());
+ }
+}
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 1485d03..4822478 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
@@ -14,92 +14,38 @@
package com.google.gerrit.server.change;
-import com.google.common.collect.Iterables;
import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.common.data.LabelType;
-import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.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.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
-import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.ChangeMessagesUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.PatchSetUtil;
-import com.google.gerrit.server.extensions.events.ReviewerDeleted;
-import com.google.gerrit.server.mail.send.DeleteReviewerSender;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
-import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
-import com.google.gerrit.server.update.BatchUpdateReviewDb;
-import com.google.gerrit.server.update.ChangeContext;
-import com.google.gerrit.server.update.Context;
import com.google.gerrit.server.update.UpdateException;
-import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@Singleton
public class DeleteReviewer implements RestModifyView<ReviewerResource, DeleteReviewerInput> {
- private static final Logger log = LoggerFactory.getLogger(DeleteReviewer.class);
private final Provider<ReviewDb> dbProvider;
- private final ApprovalsUtil approvalsUtil;
- private final PatchSetUtil psUtil;
- private final ChangeMessagesUtil cmUtil;
private final BatchUpdate.Factory batchUpdateFactory;
- private final IdentifiedUser.GenericFactory userFactory;
- private final ReviewerDeleted reviewerDeleted;
- private final Provider<IdentifiedUser> user;
- private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
- private final NotesMigration migration;
- private final NotifyUtil notifyUtil;
+ private final DeleteReviewerOp.Factory deleteReviewerOpFactory;
+ private final DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory;
@Inject
DeleteReviewer(
Provider<ReviewDb> dbProvider,
- ApprovalsUtil approvalsUtil,
- PatchSetUtil psUtil,
- ChangeMessagesUtil cmUtil,
BatchUpdate.Factory batchUpdateFactory,
- IdentifiedUser.GenericFactory userFactory,
- ReviewerDeleted reviewerDeleted,
- Provider<IdentifiedUser> user,
- DeleteReviewerSender.Factory deleteReviewerSenderFactory,
- NotesMigration migration,
- NotifyUtil notifyUtil) {
+ DeleteReviewerOp.Factory deleteReviewerOpFactory,
+ DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory) {
this.dbProvider = dbProvider;
- this.approvalsUtil = approvalsUtil;
- this.psUtil = psUtil;
- this.cmUtil = cmUtil;
this.batchUpdateFactory = batchUpdateFactory;
- this.userFactory = userFactory;
- this.reviewerDeleted = reviewerDeleted;
- this.user = user;
- this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
- this.migration = migration;
- this.notifyUtil = notifyUtil;
+ this.deleteReviewerOpFactory = deleteReviewerOpFactory;
+ this.deleteReviewerByEmailOpFactory = deleteReviewerByEmailOpFactory;
}
@Override
@@ -118,151 +64,15 @@
rsrc.getChangeResource().getProject(),
rsrc.getChangeResource().getUser(),
TimeUtil.nowTs())) {
- Op op = new Op(rsrc.getReviewerUser().getAccount(), input);
+ BatchUpdateOp op;
+ if (rsrc.isByEmail()) {
+ op = deleteReviewerByEmailOpFactory.create(rsrc.getReviewerByEmail(), input);
+ } else {
+ op = deleteReviewerOpFactory.create(rsrc.getReviewerUser().getAccount(), input);
+ }
bu.addOp(rsrc.getChange().getId(), op);
bu.execute();
}
-
return Response.none();
}
-
- private class Op implements BatchUpdateOp {
- private final Account reviewer;
- private final DeleteReviewerInput input;
- ChangeMessage changeMessage;
- Change currChange;
- PatchSet currPs;
- Map<String, Short> newApprovals = new HashMap<>();
- Map<String, Short> oldApprovals = new HashMap<>();
-
- Op(Account reviewerAccount, DeleteReviewerInput input) {
- this.reviewer = reviewerAccount;
- this.input = input;
- }
-
- @Override
- public boolean updateChange(ChangeContext ctx)
- throws AuthException, ResourceNotFoundException, OrmException {
- Account.Id reviewerId = reviewer.getId();
- if (!approvalsUtil.getReviewers(ctx.getDb(), ctx.getNotes()).all().contains(reviewerId)) {
- throw new ResourceNotFoundException();
- }
- currChange = ctx.getChange();
- currPs = psUtil.current(ctx.getDb(), ctx.getNotes());
-
- LabelTypes labelTypes = ctx.getControl().getLabelTypes();
- // removing a reviewer will remove all her votes
- for (LabelType lt : labelTypes.getLabelTypes()) {
- newApprovals.put(lt.getName(), (short) 0);
- }
-
- StringBuilder msg = new StringBuilder();
- msg.append("Removed reviewer " + reviewer.getFullName());
- StringBuilder removedVotesMsg = new StringBuilder();
- removedVotesMsg.append(" with the following votes:\n\n");
- List<PatchSetApproval> del = new ArrayList<>();
- boolean votesRemoved = false;
- for (PatchSetApproval a : approvals(ctx, reviewerId)) {
- if (ctx.getControl().canRemoveReviewer(a)) {
- del.add(a);
- if (a.getPatchSetId().equals(currPs.getId()) && a.getValue() != 0) {
- oldApprovals.put(a.getLabel(), a.getValue());
- removedVotesMsg
- .append("* ")
- .append(a.getLabel())
- .append(formatLabelValue(a.getValue()))
- .append(" by ")
- .append(userFactory.create(a.getAccountId()).getNameEmail())
- .append("\n");
- votesRemoved = true;
- }
- } else {
- throw new AuthException("delete reviewer not permitted");
- }
- }
-
- if (votesRemoved) {
- msg.append(removedVotesMsg);
- } else {
- msg.append(".");
- }
- ctx.getDb().patchSetApprovals().delete(del);
- ChangeUpdate update = ctx.getUpdate(currPs.getId());
- update.removeReviewer(reviewerId);
-
- changeMessage =
- ChangeMessagesUtil.newMessage(
- ctx, msg.toString(), ChangeMessagesUtil.TAG_DELETE_REVIEWER);
- cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage);
-
- return true;
- }
-
- @Override
- public void postUpdate(Context ctx) {
- if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
- emailReviewers(ctx.getProject(), currChange, changeMessage);
- }
- reviewerDeleted.fire(
- currChange,
- currPs,
- reviewer,
- ctx.getAccount(),
- changeMessage.getMessage(),
- newApprovals,
- oldApprovals,
- input.notify,
- ctx.getWhen());
- }
-
- private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId)
- throws OrmException {
- Change.Id changeId = ctx.getNotes().getChangeId();
- Iterable<PatchSetApproval> approvals;
- PrimaryStorage r = PrimaryStorage.of(ctx.getChange());
-
- if (migration.readChanges() && r == PrimaryStorage.REVIEW_DB) {
- // Because NoteDb and ReviewDb have different semantics for zero-value
- // approvals, we must fall back to ReviewDb as the source of truth here.
- ReviewDb db = ctx.getDb();
-
- if (db instanceof BatchUpdateReviewDb) {
- db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
- }
- db = ReviewDbUtil.unwrapDb(db);
- approvals = db.patchSetApprovals().byChange(changeId);
- } else {
- approvals = approvalsUtil.byChange(ctx.getDb(), ctx.getNotes()).values();
- }
-
- return Iterables.filter(approvals, psa -> accountId.equals(psa.getAccountId()));
- }
-
- private String formatLabelValue(short value) {
- if (value > 0) {
- return "+" + value;
- }
- return Short.toString(value);
- }
-
- private void emailReviewers(
- Project.NameKey projectName, Change change, ChangeMessage changeMessage) {
- Account.Id userId = user.get().getAccountId();
- if (userId.equals(reviewer.getId())) {
- // The user knows they removed themselves, don't bother emailing them.
- return;
- }
- try {
- DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId());
- cm.setFrom(userId);
- cm.addReviewers(Collections.singleton(reviewer.getId()));
- cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
- cm.setNotify(input.notify);
- cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
- cm.send();
- } catch (Exception err) {
- log.error("Cannot email update for change " + change.getId(), err);
- }
- }
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
new file mode 100644
index 0000000..adfe3f5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.mail.send.DeleteReviewerSender;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.Context;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.util.Collections;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DeleteReviewerByEmailOp implements BatchUpdateOp {
+ private static final Logger log = LoggerFactory.getLogger(DeleteReviewer.class);
+
+ public interface Factory {
+ DeleteReviewerByEmailOp create(Address reviewer, DeleteReviewerInput input);
+ }
+
+ private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
+ private final NotifyUtil notifyUtil;
+ private final Address reviewer;
+ private final DeleteReviewerInput input;
+
+ private ChangeMessage changeMessage;
+ private Change.Id changeId;
+
+ @Inject
+ DeleteReviewerByEmailOp(
+ DeleteReviewerSender.Factory deleteReviewerSenderFactory,
+ NotifyUtil notifyUtil,
+ @Assisted Address reviewer,
+ @Assisted DeleteReviewerInput input) {
+ this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
+ this.notifyUtil = notifyUtil;
+ this.reviewer = reviewer;
+ this.input = input;
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) throws OrmException {
+ changeId = ctx.getChange().getId();
+ PatchSet.Id psId = ctx.getChange().currentPatchSetId();
+ String msg = "Removed reviewer " + reviewer;
+ changeMessage =
+ new ChangeMessage(
+ new ChangeMessage.Key(changeId, ChangeUtil.messageUuid()),
+ ctx.getAccountId(),
+ ctx.getWhen(),
+ psId);
+ changeMessage.setMessage(msg);
+
+ ctx.getUpdate(psId).setChangeMessage(msg);
+ ctx.getUpdate(psId).removeReviewerByEmail(reviewer);
+ return true;
+ }
+
+ @Override
+ public void postUpdate(Context ctx) {
+ if (!NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
+ return;
+ }
+ try {
+ DeleteReviewerSender cm = deleteReviewerSenderFactory.create(ctx.getProject(), changeId);
+ cm.setFrom(ctx.getAccountId());
+ cm.addReviewersByEmail(Collections.singleton(reviewer));
+ cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
+ cm.setNotify(input.notify);
+ cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+ cm.send();
+ } catch (Exception err) {
+ log.error("Cannot email update for change " + changeId, err);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerOp.java
new file mode 100644
index 0000000..a255f79
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -0,0 +1,232 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.extensions.api.changes.DeleteReviewerInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.extensions.events.ReviewerDeleted;
+import com.google.gerrit.server.mail.send.DeleteReviewerSender;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.BatchUpdateReviewDb;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.Context;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DeleteReviewerOp implements BatchUpdateOp {
+ private static final Logger log = LoggerFactory.getLogger(DeleteReviewer.class);
+
+ public interface Factory {
+ DeleteReviewerOp create(Account reviewerAccount, DeleteReviewerInput input);
+ }
+
+ private final ApprovalsUtil approvalsUtil;
+ private final PatchSetUtil psUtil;
+ private final ChangeMessagesUtil cmUtil;
+ private final IdentifiedUser.GenericFactory userFactory;
+ private final ReviewerDeleted reviewerDeleted;
+ private final Provider<IdentifiedUser> user;
+ private final DeleteReviewerSender.Factory deleteReviewerSenderFactory;
+ private final NotesMigration migration;
+ private final NotifyUtil notifyUtil;
+
+ private final Account reviewer;
+ private final DeleteReviewerInput input;
+
+ ChangeMessage changeMessage;
+ Change currChange;
+ PatchSet currPs;
+ Map<String, Short> newApprovals = new HashMap<>();
+ Map<String, Short> oldApprovals = new HashMap<>();
+
+ @Inject
+ DeleteReviewerOp(
+ ApprovalsUtil approvalsUtil,
+ PatchSetUtil psUtil,
+ ChangeMessagesUtil cmUtil,
+ IdentifiedUser.GenericFactory userFactory,
+ ReviewerDeleted reviewerDeleted,
+ Provider<IdentifiedUser> user,
+ DeleteReviewerSender.Factory deleteReviewerSenderFactory,
+ NotesMigration migration,
+ NotifyUtil notifyUtil,
+ @Assisted Account reviewerAccount,
+ @Assisted DeleteReviewerInput input) {
+ this.approvalsUtil = approvalsUtil;
+ this.psUtil = psUtil;
+ this.cmUtil = cmUtil;
+ this.userFactory = userFactory;
+ this.reviewerDeleted = reviewerDeleted;
+ this.user = user;
+ this.deleteReviewerSenderFactory = deleteReviewerSenderFactory;
+ this.migration = migration;
+ this.notifyUtil = notifyUtil;
+
+ this.reviewer = reviewerAccount;
+ this.input = input;
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx)
+ throws AuthException, ResourceNotFoundException, OrmException {
+ Account.Id reviewerId = reviewer.getId();
+ if (!approvalsUtil.getReviewers(ctx.getDb(), ctx.getNotes()).all().contains(reviewerId)) {
+ throw new ResourceNotFoundException();
+ }
+ currChange = ctx.getChange();
+ currPs = psUtil.current(ctx.getDb(), ctx.getNotes());
+
+ LabelTypes labelTypes = ctx.getControl().getLabelTypes();
+ // removing a reviewer will remove all her votes
+ for (LabelType lt : labelTypes.getLabelTypes()) {
+ newApprovals.put(lt.getName(), (short) 0);
+ }
+
+ StringBuilder msg = new StringBuilder();
+ msg.append("Removed reviewer " + reviewer.getFullName());
+ StringBuilder removedVotesMsg = new StringBuilder();
+ removedVotesMsg.append(" with the following votes:\n\n");
+ List<PatchSetApproval> del = new ArrayList<>();
+ boolean votesRemoved = false;
+ for (PatchSetApproval a : approvals(ctx, reviewerId)) {
+ if (ctx.getControl().canRemoveReviewer(a)) {
+ del.add(a);
+ if (a.getPatchSetId().equals(currPs.getId()) && a.getValue() != 0) {
+ oldApprovals.put(a.getLabel(), a.getValue());
+ removedVotesMsg
+ .append("* ")
+ .append(a.getLabel())
+ .append(formatLabelValue(a.getValue()))
+ .append(" by ")
+ .append(userFactory.create(a.getAccountId()).getNameEmail())
+ .append("\n");
+ votesRemoved = true;
+ }
+ } else {
+ throw new AuthException("delete reviewer not permitted");
+ }
+ }
+
+ if (votesRemoved) {
+ msg.append(removedVotesMsg);
+ } else {
+ msg.append(".");
+ }
+ ctx.getDb().patchSetApprovals().delete(del);
+ ChangeUpdate update = ctx.getUpdate(currPs.getId());
+ update.removeReviewer(reviewerId);
+
+ changeMessage =
+ ChangeMessagesUtil.newMessage(ctx, msg.toString(), ChangeMessagesUtil.TAG_DELETE_REVIEWER);
+ cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage);
+
+ return true;
+ }
+
+ @Override
+ public void postUpdate(Context ctx) {
+ if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) {
+ emailReviewers(ctx.getProject(), currChange, changeMessage);
+ }
+ reviewerDeleted.fire(
+ currChange,
+ currPs,
+ reviewer,
+ ctx.getAccount(),
+ changeMessage.getMessage(),
+ newApprovals,
+ oldApprovals,
+ input.notify,
+ ctx.getWhen());
+ }
+
+ private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId)
+ throws OrmException {
+ Change.Id changeId = ctx.getNotes().getChangeId();
+ Iterable<PatchSetApproval> approvals;
+ PrimaryStorage r = PrimaryStorage.of(ctx.getChange());
+
+ if (migration.readChanges() && r == PrimaryStorage.REVIEW_DB) {
+ // Because NoteDb and ReviewDb have different semantics for zero-value
+ // approvals, we must fall back to ReviewDb as the source of truth here.
+ ReviewDb db = ctx.getDb();
+
+ if (db instanceof BatchUpdateReviewDb) {
+ db = ((BatchUpdateReviewDb) db).unsafeGetDelegate();
+ }
+ db = ReviewDbUtil.unwrapDb(db);
+ approvals = db.patchSetApprovals().byChange(changeId);
+ } else {
+ approvals = approvalsUtil.byChange(ctx.getDb(), ctx.getNotes()).values();
+ }
+
+ return Iterables.filter(approvals, psa -> accountId.equals(psa.getAccountId()));
+ }
+
+ private String formatLabelValue(short value) {
+ if (value > 0) {
+ return "+" + value;
+ }
+ return Short.toString(value);
+ }
+
+ private void emailReviewers(
+ Project.NameKey projectName, Change change, ChangeMessage changeMessage) {
+ Account.Id userId = user.get().getAccountId();
+ if (userId.equals(reviewer.getId())) {
+ // The user knows they removed themselves, don't bother emailing them.
+ return;
+ }
+ try {
+ DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId());
+ cm.setFrom(userId);
+ cm.addReviewers(Collections.singleton(reviewer.getId()));
+ cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn());
+ cm.setNotify(input.notify);
+ cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails));
+ cm.send();
+ } catch (Exception err) {
+ log.error("Cannot email update for change " + change.getId(), err);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
index 732848e..166197e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -69,8 +69,15 @@
public BinaryResult getContent(ProjectState project, ObjectId revstr, String path)
throws ResourceNotFoundException, IOException {
- try (Repository repo = openRepository(project);
- RevWalk rw = new RevWalk(repo)) {
+ try (Repository repo = openRepository(project)) {
+ return getContent(repo, project, revstr, path);
+ }
+ }
+
+ public BinaryResult getContent(
+ Repository repo, ProjectState project, ObjectId revstr, String path)
+ throws IOException, ResourceNotFoundException {
+ try (RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(revstr);
ObjectReader reader = rw.getObjectReader();
TreeWalk tw = TreeWalk.forPath(reader, path, commit.getTree());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
index 60a4daf..b25b588 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -48,9 +48,14 @@
Map<String, FileInfo> toFileInfoMap(Change change, RevId revision, @Nullable PatchSet base)
throws PatchListNotAvailableException {
+ ObjectId objectId = ObjectId.fromString(revision.get());
+ return toFileInfoMap(change, objectId, base);
+ }
+
+ Map<String, FileInfo> toFileInfoMap(Change change, ObjectId objectId, @Nullable PatchSet base)
+ throws PatchListNotAvailableException {
ObjectId a = (base == null) ? null : ObjectId.fromString(base.getRevision().get());
- ObjectId b = ObjectId.fromString(revision.get());
- return toFileInfoMap(change, new PatchListKey(a, b, Whitespace.IGNORE_NONE));
+ return toFileInfoMap(change, new PatchListKey(a, objectId, Whitespace.IGNORE_NONE));
}
Map<String, FileInfo> toFileInfoMap(Change change, RevId revision, int parent)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FixResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FixResource.java
new file mode 100644
index 0000000..08e2785
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FixResource.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.FixReplacement;
+import com.google.inject.TypeLiteral;
+import java.util.List;
+
+public class FixResource implements RestResource {
+ public static final TypeLiteral<RestView<FixResource>> FIX_KIND =
+ new TypeLiteral<RestView<FixResource>>() {};
+
+ private final List<FixReplacement> fixReplacements;
+ private final RevisionResource revisionResource;
+
+ public FixResource(RevisionResource revisionResource, List<FixReplacement> fixReplacements) {
+ this.fixReplacements = fixReplacements;
+ this.revisionResource = revisionResource;
+ }
+
+ public List<FixReplacement> getFixReplacements() {
+ return fixReplacements;
+ }
+
+ public RevisionResource getRevisionResource() {
+ return revisionResource;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Fixes.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Fixes.java
new file mode 100644
index 0000000..af9f60a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Fixes.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.FixSuggestion;
+import com.google.gerrit.reviewdb.client.RobotComment;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.List;
+import java.util.Objects;
+
+@Singleton
+public class Fixes implements ChildCollection<RevisionResource, FixResource> {
+
+ private final DynamicMap<RestView<FixResource>> views;
+ private final CommentsUtil commentsUtil;
+
+ @Inject
+ Fixes(DynamicMap<RestView<FixResource>> views, CommentsUtil commentsUtil) {
+ this.views = views;
+ this.commentsUtil = commentsUtil;
+ }
+
+ @Override
+ public RestView<RevisionResource> list() throws ResourceNotFoundException {
+ throw new ResourceNotFoundException();
+ }
+
+ @Override
+ public FixResource parse(RevisionResource revisionResource, IdString id)
+ throws ResourceNotFoundException, OrmException {
+ String fixId = id.get();
+ ChangeNotes changeNotes = revisionResource.getNotes();
+
+ List<RobotComment> robotComments =
+ commentsUtil.robotCommentsByPatchSet(changeNotes, revisionResource.getPatchSet().getId());
+ for (RobotComment robotComment : robotComments) {
+ for (FixSuggestion fixSuggestion : robotComment.fixSuggestions) {
+ if (Objects.equals(fixId, fixSuggestion.fixId)) {
+ return new FixResource(revisionResource, fixSuggestion.replacements);
+ }
+ }
+ }
+ throw new ResourceNotFoundException(id);
+ }
+
+ @Override
+ public DynamicMap<RestView<FixResource>> views() {
+ return views;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
index 27ec89d..1dba58c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
@@ -19,6 +19,7 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.mail.Address;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -48,11 +49,16 @@
@Override
public List<ReviewerInfo> apply(ChangeResource rsrc) throws OrmException {
- Map<Account.Id, ReviewerResource> reviewers = new LinkedHashMap<>();
+ Map<String, ReviewerResource> reviewers = new LinkedHashMap<>();
ReviewDb db = dbProvider.get();
for (Account.Id accountId : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) {
- if (!reviewers.containsKey(accountId)) {
- reviewers.put(accountId, resourceFactory.create(rsrc, accountId));
+ if (!reviewers.containsKey(accountId.toString())) {
+ reviewers.put(accountId.toString(), resourceFactory.create(rsrc, accountId));
+ }
+ }
+ for (Address adr : rsrc.getNotes().getReviewersByEmail().all()) {
+ if (!reviewers.containsKey(adr.toString())) {
+ reviewers.put(adr.toString(), new ReviewerResource(rsrc, adr));
}
}
return json.format(reviewers.values());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java
index d0c8ca0..5aaee56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java
@@ -20,6 +20,7 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.mail.Address;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -54,11 +55,16 @@
throw new MethodNotAllowedException("Cannot list reviewers on non-current patch set");
}
- Map<Account.Id, ReviewerResource> reviewers = new LinkedHashMap<>();
+ Map<String, ReviewerResource> reviewers = new LinkedHashMap<>();
ReviewDb db = dbProvider.get();
for (Account.Id accountId : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) {
- if (!reviewers.containsKey(accountId)) {
- reviewers.put(accountId, resourceFactory.create(rsrc, accountId));
+ if (!reviewers.containsKey(accountId.toString())) {
+ reviewers.put(accountId.toString(), resourceFactory.create(rsrc, accountId));
+ }
+ }
+ for (Address address : rsrc.getNotes().getReviewersByEmail().all()) {
+ if (!reviewers.containsKey(address.toString())) {
+ reviewers.put(address.toString(), new ReviewerResource(rsrc, address));
}
}
return json.format(reviewers.values());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index d67f8ce..119051e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -31,9 +31,7 @@
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
-import com.google.gerrit.server.git.IntegrationException;
import com.google.gerrit.server.git.strategy.SubmitDryRun;
-import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
@@ -45,7 +43,6 @@
import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
-import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -173,31 +170,6 @@
}
}
- private class Loader implements Callable<Boolean> {
- private final EntryKey key;
- private final Branch.NameKey dest;
- private final Repository repo;
-
- Loader(EntryKey key, Branch.NameKey dest, Repository repo) {
- this.key = key;
- this.dest = dest;
- this.repo = repo;
- }
-
- @Override
- public Boolean call() throws NoSuchProjectException, IntegrationException, IOException {
- if (key.into.equals(ObjectId.zeroId())) {
- return true; // Assume yes on new branch.
- }
- try (CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
- Set<RevCommit> accepted = SubmitDryRun.getAlreadyAccepted(repo, rw);
- accepted.add(rw.parseCommit(key.into));
- accepted.addAll(Arrays.asList(rw.parseCommit(key.commit).getParents()));
- return submitDryRun.run(key.submitType, repo, rw, dest, key.into, key.commit, accepted);
- }
- }
- }
-
public static class MergeabilityWeigher implements Weigher<EntryKey, Boolean> {
@Override
public int weigh(EntryKey k, Boolean v) {
@@ -229,7 +201,20 @@
ObjectId into = intoRef != null ? intoRef.getObjectId() : ObjectId.zeroId();
EntryKey key = new EntryKey(commit, into, submitType, mergeStrategy);
try {
- return cache.get(key, new Loader(key, dest, repo));
+ return cache.get(
+ key,
+ () -> {
+ if (key.into.equals(ObjectId.zeroId())) {
+ return true; // Assume yes on new branch.
+ }
+ try (CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
+ Set<RevCommit> accepted = SubmitDryRun.getAlreadyAccepted(repo, rw);
+ accepted.add(rw.parseCommit(key.into));
+ accepted.addAll(Arrays.asList(rw.parseCommit(key.commit).getParents()));
+ return submitDryRun.run(
+ key.submitType, repo, rw, dest, key.into, key.commit, accepted);
+ }
+ });
} catch (ExecutionException | UncheckedExecutionException e) {
log.error(
String.format(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index aca6ef1..dcde8d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -19,6 +19,7 @@
import static com.google.gerrit.server.change.CommentResource.COMMENT_KIND;
import static com.google.gerrit.server.change.DraftCommentResource.DRAFT_COMMENT_KIND;
import static com.google.gerrit.server.change.FileResource.FILE_KIND;
+import static com.google.gerrit.server.change.FixResource.FIX_KIND;
import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
import static com.google.gerrit.server.change.RobotCommentResource.ROBOT_COMMENT_KIND;
@@ -40,12 +41,14 @@
bind(DraftComments.class);
bind(Comments.class);
bind(RobotComments.class);
+ bind(Fixes.class);
bind(Files.class);
bind(Votes.class);
DynamicMap.mapOf(binder(), CHANGE_KIND);
DynamicMap.mapOf(binder(), COMMENT_KIND);
DynamicMap.mapOf(binder(), ROBOT_COMMENT_KIND);
+ DynamicMap.mapOf(binder(), FIX_KIND);
DynamicMap.mapOf(binder(), DRAFT_COMMENT_KIND);
DynamicMap.mapOf(binder(), FILE_KIND);
DynamicMap.mapOf(binder(), REVIEWER_KIND);
@@ -82,6 +85,8 @@
post(CHANGE_KIND, "index").to(Index.class);
post(CHANGE_KIND, "rebuild.notedb").to(Rebuild.class);
post(CHANGE_KIND, "move").to(Move.class);
+ put(CHANGE_KIND, "private").to(PutPrivate.class);
+ delete(CHANGE_KIND, "private").to(DeletePrivate.class);
post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class);
@@ -128,6 +133,8 @@
child(REVISION_KIND, "robotcomments").to(RobotComments.class);
get(ROBOT_COMMENT_KIND).to(GetRobotComment.class);
+ child(REVISION_KIND, "fixes").to(Fixes.class);
+ post(FIX_KIND, "apply").to(ApplyFix.class);
child(REVISION_KIND, "files").to(Files.class);
put(FILE_KIND, "reviewed").to(PutReviewed.class);
@@ -159,5 +166,7 @@
factory(SetAssigneeOp.Factory.class);
factory(SetHashtagsOp.Factory.class);
factory(ChangeResource.Factory.class);
+ factory(DeleteReviewerOp.Factory.class);
+ factory(DeleteReviewerByEmailOp.Factory.class);
}
}
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 7cf62a0..88e9d42 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
@@ -50,13 +50,12 @@
import com.google.gerrit.server.update.Context;
import com.google.gerrit.server.update.RepoContext;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -65,7 +64,7 @@
private static final Logger log = LoggerFactory.getLogger(PatchSetInserter.class);
public interface Factory {
- PatchSetInserter create(ChangeControl ctl, PatchSet.Id psId, RevCommit commit);
+ PatchSetInserter create(ChangeControl ctl, PatchSet.Id psId, ObjectId commitId);
}
// Injected fields.
@@ -80,7 +79,7 @@
// Assisted-injected fields.
private final PatchSet.Id psId;
- private final RevCommit commit;
+ private final ObjectId commitId;
// Read prior to running the batch update, so must only be used during
// updateRepo; updateChange and later must use the control from the
// ChangeContext.
@@ -106,7 +105,7 @@
private ChangeMessage changeMessage;
private ReviewerSet oldReviewers;
- @AssistedInject
+ @Inject
public PatchSetInserter(
ApprovalsUtil approvalsUtil,
ApprovalCopier approvalCopier,
@@ -118,7 +117,7 @@
RevisionCreated revisionCreated,
@Assisted ChangeControl ctl,
@Assisted PatchSet.Id psId,
- @Assisted RevCommit commit) {
+ @Assisted ObjectId commitId) {
this.approvalsUtil = approvalsUtil;
this.approvalCopier = approvalCopier;
this.cmUtil = cmUtil;
@@ -130,7 +129,7 @@
this.origCtl = ctl;
this.psId = psId;
- this.commit = commit;
+ this.commitId = commitId.copy();
}
public PatchSet.Id getPatchSetId() {
@@ -210,7 +209,7 @@
validate(ctx);
ctx.addRefUpdate(
new ReceiveCommand(
- ObjectId.zeroId(), commit, getPatchSetId().toRefName(), ReceiveCommand.Type.CREATE));
+ ObjectId.zeroId(), commitId, getPatchSetId().toRefName(), ReceiveCommand.Type.CREATE));
}
@Override
@@ -243,7 +242,7 @@
ctx.getRevWalk(),
ctx.getUpdate(psId),
psId,
- commit,
+ commitId,
draft,
newGroups,
null,
@@ -264,7 +263,8 @@
changeMessage.setMessage(message);
}
- patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId);
+ patchSetInfo =
+ patchSetInfoFactory.get(ctx.getRevWalk(), ctx.getRevWalk().parseCommit(commitId), psId);
if (change.getStatus() != Change.Status.DRAFT && !allowClosed) {
change.setStatus(Change.Status.NEW);
}
@@ -311,18 +311,17 @@
}
String refName = getPatchSetId().toRefName();
- CommitReceivedEvent event =
+ try (CommitReceivedEvent event =
new CommitReceivedEvent(
new ReceiveCommand(
ObjectId.zeroId(),
- commit.getId(),
+ commitId,
refName.substring(0, refName.lastIndexOf('/') + 1) + "new"),
origCtl.getProjectControl().getProject(),
origCtl.getRefControl().getRefName(),
- commit,
- ctx.getIdentifiedUser());
-
- try {
+ ctx.getRevWalk().getObjectReader(),
+ commitId,
+ ctx.getIdentifiedUser())) {
commitValidatorsFactory
.create(validatePolicy, origCtl.getRefControl(), new NoSshInfo(), ctx.getRepository())
.validate(event);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
index 5aa41b1..ebe8f7e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
@@ -22,6 +22,8 @@
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.UpdateException;
import com.google.inject.Inject;
@@ -47,7 +49,9 @@
@Override
public Response<ImmutableSortedSet<String>> apply(ChangeResource req, HashtagsInput input)
- throws RestApiException, UpdateException {
+ throws RestApiException, UpdateException, PermissionBackendException {
+ req.permissions().check(ChangePermission.EDIT_HASHTAGS);
+
try (BatchUpdate bu =
batchUpdateFactory.create(
db.get(), req.getChange().getProject(), req.getControl().getUser(), TimeUtil.nowTs())) {
@@ -59,9 +63,9 @@
}
@Override
- public UiAction.Description getDescription(ChangeResource resource) {
+ public UiAction.Description getDescription(ChangeResource rsrc) {
return new UiAction.Description()
.setLabel("Edit Hashtags")
- .setVisible(resource.getControl().canEditHashtags());
+ .setVisible(rsrc.permissions().testOrFalse(ChangePermission.EDIT_HASHTAGS));
}
}
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 76cc7e8..5d0b789 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
@@ -19,6 +19,7 @@
import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
@@ -50,6 +51,7 @@
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
import com.google.gerrit.extensions.api.changes.ReviewResult;
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
+import com.google.gerrit.extensions.client.Comment.Range;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.Side;
import com.google.gerrit.extensions.common.AccountInfo;
@@ -82,10 +84,13 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.OutputFormat;
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.GerritServerConfig;
import com.google.gerrit.server.extensions.events.CommentAdded;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.notedb.NotesMigration;
@@ -93,16 +98,19 @@
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.BatchUpdate.Factory;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.Context;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.util.LabelVote;
+import com.google.gson.Gson;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
@@ -113,13 +121,17 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.OptionalInt;
import java.util.Set;
+import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class PostReview implements RestModifyView<RevisionResource, ReviewInput> {
private static final Logger log = LoggerFactory.getLogger(PostReview.class);
+ private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
+ private static final int DEFAULT_ROBOT_COMMENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024;
private final Provider<ReviewDb> db;
private final BatchUpdate.Factory batchUpdateFactory;
@@ -136,11 +148,12 @@
private final PostReviewers postReviewers;
private final NotesMigration migration;
private final NotifyUtil notifyUtil;
+ private final Config gerritConfig;
@Inject
PostReview(
Provider<ReviewDb> db,
- BatchUpdate.Factory batchUpdateFactory,
+ Factory batchUpdateFactory,
ChangesCollection changes,
ChangeData.Factory changeDataFactory,
ApprovalsUtil approvalsUtil,
@@ -153,7 +166,8 @@
CommentAdded commentAdded,
PostReviewers postReviewers,
NotesMigration migration,
- NotifyUtil notifyUtil) {
+ NotifyUtil notifyUtil,
+ @GerritServerConfig Config gerritConfig) {
this.db = db;
this.batchUpdateFactory = batchUpdateFactory;
this.changes = changes;
@@ -169,6 +183,7 @@
this.postReviewers = postReviewers;
this.migration = migration;
this.notifyUtil = notifyUtil;
+ this.gerritConfig = gerritConfig;
}
@Override
@@ -313,14 +328,18 @@
ListMultimap<RecipientType, Account.Id> accountsToNotify) {
List<Account.Id> to = new ArrayList<>();
List<Account.Id> cc = new ArrayList<>();
+ List<Address> toByEmail = new ArrayList<>();
+ List<Address> ccByEmail = new ArrayList<>();
for (PostReviewers.Addition addition : reviewerAdditions) {
if (addition.op.state == ReviewerState.REVIEWER) {
to.addAll(addition.op.reviewers.keySet());
+ toByEmail.addAll(addition.op.reviewersByEmail);
} else if (addition.op.state == ReviewerState.CC) {
cc.addAll(addition.op.reviewers.keySet());
+ ccByEmail.addAll(addition.op.reviewersByEmail);
}
}
- postReviewers.emailReviewers(change, to, cc, notify, accountsToNotify);
+ postReviewers.emailReviewers(change, to, cc, toByEmail, ccByEmail, notify, accountsToNotify);
}
private RevisionResource onBehalfOf(RevisionResource rev, ReviewInput in)
@@ -425,7 +444,8 @@
}
}
- private <T extends CommentInput> void cleanUpComments(Map<String, List<T>> commentsPerPath) {
+ private static <T extends CommentInput> void cleanUpComments(
+ Map<String, List<T>> commentsPerPath) {
Iterator<List<T>> mapValueIterator = commentsPerPath.values().iterator();
while (mapValueIterator.hasNext()) {
List<T> comments = mapValueIterator.next();
@@ -441,7 +461,7 @@
}
}
- private <T extends CommentInput> void cleanUpComments(List<T> comments) {
+ private static <T extends CommentInput> void cleanUpComments(List<T> comments) {
Iterator<T> commentsIterator = comments.iterator();
while (commentsIterator.hasNext()) {
T comment = commentsIterator.next();
@@ -480,7 +500,7 @@
return new HashSet<>(changeData.filePaths(revision.getPatchSet()));
}
- private void ensurePathRefersToAvailableOrMagicFile(
+ private static void ensurePathRefersToAvailableOrMagicFile(
String path, Set<String> availableFilePaths, PatchSet.Id patchSetId)
throws BadRequestException {
if (!availableFilePaths.contains(path) && !Patch.isMagic(path)) {
@@ -489,14 +509,15 @@
}
}
- private void ensureLineIsNonNegative(Integer line, String path) throws BadRequestException {
+ private static void ensureLineIsNonNegative(Integer line, String path)
+ throws BadRequestException {
if (line != null && line < 0) {
throw new BadRequestException(
String.format("negative line number %d not allowed on %s", line, path));
}
}
- private <T extends CommentInput> void ensureCommentNotOnMagicFilesOfAutoMerge(
+ private static <T extends CommentInput> void ensureCommentNotOnMagicFilesOfAutoMerge(
String path, T comment) throws BadRequestException {
if (Patch.isMagic(path) && comment.side == Side.PARENT && comment.parent == null) {
throw new BadRequestException(String.format("cannot comment on %s on auto-merge", path));
@@ -510,6 +531,7 @@
for (Map.Entry<String, List<RobotCommentInput>> e : in.entrySet()) {
String commentPath = e.getKey();
for (RobotCommentInput c : e.getValue()) {
+ ensureSizeOfJsonInputIsWithinBounds(c);
ensureRobotIdIsSet(c.robotId, commentPath);
ensureRobotRunIdIsSet(c.robotRunId, commentPath);
ensureFixSuggestionsAreAddable(c.fixSuggestions, commentPath);
@@ -518,14 +540,41 @@
checkComments(revision, in);
}
- private void ensureRobotIdIsSet(String robotId, String commentPath) throws BadRequestException {
+ private void ensureSizeOfJsonInputIsWithinBounds(RobotCommentInput robotCommentInput)
+ throws BadRequestException {
+ OptionalInt robotCommentSizeLimit = getRobotCommentSizeLimit();
+ if (robotCommentSizeLimit.isPresent()) {
+ int sizeLimit = robotCommentSizeLimit.getAsInt();
+ byte[] robotCommentBytes = GSON.toJson(robotCommentInput).getBytes(StandardCharsets.UTF_8);
+ int robotCommentSize = robotCommentBytes.length;
+ if (robotCommentSize > sizeLimit) {
+ throw new BadRequestException(
+ String.format(
+ "Size %d (bytes) of robot comment is greater than limit %d (bytes)",
+ robotCommentSize, sizeLimit));
+ }
+ }
+ }
+
+ private OptionalInt getRobotCommentSizeLimit() {
+ int robotCommentSizeLimit =
+ gerritConfig.getInt(
+ "change", "robotCommentSizeLimit", DEFAULT_ROBOT_COMMENT_SIZE_LIMIT_IN_BYTES);
+ if (robotCommentSizeLimit <= 0) {
+ return OptionalInt.empty();
+ }
+ return OptionalInt.of(robotCommentSizeLimit);
+ }
+
+ private static void ensureRobotIdIsSet(String robotId, String commentPath)
+ throws BadRequestException {
if (robotId == null) {
throw new BadRequestException(
String.format("robotId is missing for robot comment on %s", commentPath));
}
}
- private void ensureRobotRunIdIsSet(String robotRunId, String commentPath)
+ private static void ensureRobotRunIdIsSet(String robotRunId, String commentPath)
throws BadRequestException {
if (robotRunId == null) {
throw new BadRequestException(
@@ -533,7 +582,7 @@
}
}
- private void ensureFixSuggestionsAreAddable(
+ private static void ensureFixSuggestionsAreAddable(
List<FixSuggestionInfo> fixSuggestionInfos, String commentPath) throws BadRequestException {
if (fixSuggestionInfos == null) {
return;
@@ -545,7 +594,7 @@
}
}
- private void ensureDescriptionIsSet(String commentPath, String description)
+ private static void ensureDescriptionIsSet(String commentPath, String description)
throws BadRequestException {
if (description == null) {
throw new BadRequestException(
@@ -555,20 +604,25 @@
}
}
- private void ensureFixReplacementsAreAddable(
+ private static void ensureFixReplacementsAreAddable(
String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
ensureReplacementsArePresent(commentPath, fixReplacementInfos);
for (FixReplacementInfo fixReplacementInfo : fixReplacementInfos) {
ensureReplacementPathIsSet(commentPath, fixReplacementInfo.path);
- ensureReplacementPathRefersToFileOfComment(commentPath, fixReplacementInfo.path);
ensureRangeIsSet(commentPath, fixReplacementInfo.range);
ensureRangeIsValid(commentPath, fixReplacementInfo.range);
ensureReplacementStringIsSet(commentPath, fixReplacementInfo.replacement);
}
+
+ Map<String, List<FixReplacementInfo>> replacementsPerFilePath =
+ fixReplacementInfos.stream().collect(groupingBy(fixReplacement -> fixReplacement.path));
+ for (List<FixReplacementInfo> sameFileReplacements : replacementsPerFilePath.values()) {
+ ensureRangesDoNotOverlap(commentPath, sameFileReplacements);
+ }
}
- private void ensureReplacementsArePresent(
+ private static void ensureReplacementsArePresent(
String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
if (fixReplacementInfos == null || fixReplacementInfos.isEmpty()) {
throw new BadRequestException(
@@ -579,7 +633,7 @@
}
}
- private void ensureReplacementPathIsSet(String commentPath, String replacementPath)
+ private static void ensureReplacementPathIsSet(String commentPath, String replacementPath)
throws BadRequestException {
if (replacementPath == null) {
throw new BadRequestException(
@@ -589,20 +643,7 @@
}
}
- private void ensureReplacementPathRefersToFileOfComment(
- String commentPath, String replacementPath) throws BadRequestException {
- if (!Objects.equals(commentPath, replacementPath)) {
- throw new BadRequestException(
- String.format(
- "Replacements may only be "
- + "specified for the file %s on which the robot comment was added",
- commentPath));
- }
- }
-
- private void ensureRangeIsSet(
- String commentPath, com.google.gerrit.extensions.client.Comment.Range range)
- throws BadRequestException {
+ private static void ensureRangeIsSet(String commentPath, Range range) throws BadRequestException {
if (range == null) {
throw new BadRequestException(
String.format(
@@ -610,8 +651,7 @@
}
}
- private void ensureRangeIsValid(
- String commentPath, com.google.gerrit.extensions.client.Comment.Range range)
+ private static void ensureRangeIsValid(String commentPath, Range range)
throws BadRequestException {
if (range == null) {
return;
@@ -628,7 +668,7 @@
}
}
- private void ensureReplacementStringIsSet(String commentPath, String replacement)
+ private static void ensureReplacementStringIsSet(String commentPath, String replacement)
throws BadRequestException {
if (replacement == null) {
throw new BadRequestException(
@@ -639,6 +679,28 @@
}
}
+ private static void ensureRangesDoNotOverlap(
+ String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException {
+ List<Range> sortedRanges =
+ fixReplacementInfos
+ .stream()
+ .map(fixReplacementInfo -> fixReplacementInfo.range)
+ .sorted()
+ .collect(toList());
+
+ int previousEndLine = 0;
+ int previousOffset = -1;
+ for (Range range : sortedRanges) {
+ if (range.startLine < previousEndLine
+ || (range.startLine == previousEndLine && range.startCharacter < previousOffset)) {
+ throw new BadRequestException(
+ String.format("Replacements overlap for the robot comment on %s", commentPath));
+ }
+ previousEndLine = range.endLine;
+ previousOffset = range.endCharacter;
+ }
+ }
+
/** Used to compare Comments with CommentInput comments. */
@AutoValue
abstract static class CommentSetEntry {
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 7031d51..f3d8ab1 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
@@ -23,6 +23,7 @@
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.errors.NoSuchGroupException;
@@ -32,6 +33,7 @@
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -42,6 +44,7 @@
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
@@ -54,10 +57,14 @@
import com.google.gerrit.server.extensions.events.ReviewerAdded;
import com.google.gerrit.server.group.GroupsCollection;
import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.mail.send.AddReviewerSender;
+import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -104,6 +111,8 @@
private final NotesMigration migration;
private final AccountCache accountCache;
private final NotifyUtil notifyUtil;
+ private final ProjectCache projectCache;
+ private final Provider<AnonymousUser> anonymousProvider;
@Inject
PostReviewers(
@@ -124,7 +133,9 @@
ReviewerAdded reviewerAdded,
NotesMigration migration,
AccountCache accountCache,
- NotifyUtil notifyUtil) {
+ NotifyUtil notifyUtil,
+ ProjectCache projectCache,
+ Provider<AnonymousUser> anonymousProvider) {
this.accounts = accounts;
this.reviewerFactory = reviewerFactory;
this.approvalsUtil = approvalsUtil;
@@ -143,6 +154,8 @@
this.migration = migration;
this.accountCache = accountCache;
this.notifyUtil = notifyUtil;
+ this.projectCache = projectCache;
+ this.anonymousProvider = anonymousProvider;
}
@Override
@@ -167,6 +180,7 @@
return addition.result;
}
+ // TODO(hiesel) Refactor this as it starts to become unreadable
public Addition prepareApplication(
ChangeResource rsrc, AddReviewerInput input, boolean allowGroup)
throws OrmException, RestApiException, IOException {
@@ -174,17 +188,29 @@
try {
accountId = accounts.parse(input.reviewer).getAccountId();
} catch (UnprocessableEntityException e) {
+ boolean enableReviewerByEmail =
+ projectCache.checkedGet(rsrc.getProject()).isEnableReviewerByEmail();
if (allowGroup) {
try {
return putGroup(rsrc, input);
} catch (UnprocessableEntityException e2) {
- throw new UnprocessableEntityException(
- MessageFormat.format(
- ChangeMessages.get().reviewerNotFoundUserOrGroup, input.reviewer));
+ if (!enableReviewerByEmail) {
+ throw new UnprocessableEntityException(
+ MessageFormat.format(
+ ChangeMessages.get().reviewerNotFoundUserOrGroup, input.reviewer));
+ }
}
}
- throw new UnprocessableEntityException(
- MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer));
+ if (!enableReviewerByEmail) {
+ throw new UnprocessableEntityException(
+ MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer));
+ }
+ return putAccountByEmail(
+ input.reviewer,
+ rsrc,
+ input.state(),
+ input.notify,
+ notifyUtil.resolveAccounts(input.notifyDetails));
}
return putAccount(
input.reviewer,
@@ -199,6 +225,7 @@
user.getUserName(),
revision.getChangeResource(),
ImmutableMap.of(user.getAccountId(), revision.getControl()),
+ null,
CC,
NotifyHandling.NONE,
ImmutableListMultimap.of());
@@ -218,6 +245,7 @@
reviewer,
rsrc.getChangeResource(),
ImmutableMap.of(member.getId(), control),
+ null,
state,
notify,
accountsToNotify);
@@ -228,6 +256,32 @@
throw new UnprocessableEntityException(String.format("Account of %s is inactive.", reviewer));
}
+ private Addition putAccountByEmail(
+ String reviewer,
+ ChangeResource rsrc,
+ ReviewerState state,
+ NotifyHandling notify,
+ ListMultimap<RecipientType, Account.Id> accountsToNotify)
+ throws UnprocessableEntityException, OrmException, BadRequestException {
+ if (!rsrc.getControl().forUser(anonymousProvider.get()).isVisible(dbProvider.get())) {
+ throw new BadRequestException("change is not publicly visible");
+ }
+ if (!migration.readChanges()) {
+ throw new BadRequestException("feature only supported in NoteDb");
+ }
+ Address adr;
+ try {
+ adr = Address.parse(reviewer);
+ } catch (IllegalArgumentException e) {
+ throw new UnprocessableEntityException(String.format("email invalid %s", reviewer));
+ }
+ if (!OutgoingEmailValidator.isValid(adr.getEmail())) {
+ throw new UnprocessableEntityException(String.format("email invalid %s", reviewer));
+ }
+ return new Addition(
+ reviewer, rsrc, null, ImmutableList.of(adr), state, notify, accountsToNotify);
+ }
+
private Addition putGroup(ChangeResource rsrc, AddReviewerInput input)
throws RestApiException, OrmException, IOException {
GroupDescription.Basic group = groupsCollection.parseInternal(input.reviewer);
@@ -283,6 +337,7 @@
input.reviewer,
rsrc,
reviewers,
+ null,
input.state(),
input.notify,
notifyUtil.resolveAccounts(input.notifyDetails));
@@ -314,26 +369,28 @@
final Op op;
private final Map<Account.Id, ChangeControl> reviewers;
+ private final Collection<Address> reviewersByEmail;
protected Addition(String reviewer) {
- this(reviewer, null, null, REVIEWER, null, ImmutableListMultimap.of());
+ this(reviewer, null, null, null, REVIEWER, null, ImmutableListMultimap.of());
}
protected Addition(
String reviewer,
ChangeResource rsrc,
- Map<Account.Id, ChangeControl> reviewers,
+ @Nullable Map<Account.Id, ChangeControl> reviewers,
+ @Nullable Collection<Address> reviewersByEmail,
ReviewerState state,
NotifyHandling notify,
ListMultimap<RecipientType, Account.Id> accountsToNotify) {
result = new AddReviewerResult(reviewer);
- if (reviewers == null) {
- this.reviewers = ImmutableMap.of();
+ this.reviewers = reviewers == null ? ImmutableMap.of() : reviewers;
+ this.reviewersByEmail = reviewersByEmail == null ? ImmutableList.of() : reviewersByEmail;
+ if (reviewers == null && reviewersByEmail == null) {
op = null;
return;
}
- this.reviewers = reviewers;
- op = new Op(rsrc, reviewers, state, notify, accountsToNotify);
+ op = new Op(rsrc, this.reviewers, this.reviewersByEmail, state, notify, accountsToNotify);
}
void gatherResults() throws OrmException {
@@ -345,6 +402,9 @@
result.ccs.add(json.format(new ReviewerInfo(accountId.get()), reviewers.get(accountId)));
}
accountLoaderFactory.create(true).fill(result.ccs);
+ for (Address a : reviewersByEmail) {
+ result.ccs.add(new AccountInfo(a.getName(), a.getEmail()));
+ }
} else {
result.reviewers = Lists.newArrayListWithCapacity(op.addedReviewers.size());
for (PatchSetApproval psa : op.addedReviewers) {
@@ -356,17 +416,22 @@
ImmutableList.of(psa)));
}
accountLoaderFactory.create(true).fill(result.reviewers);
+ for (Address a : reviewersByEmail) {
+ result.reviewers.add(ReviewerInfo.byEmail(a.getName(), a.getEmail()));
+ }
}
}
}
public class Op implements BatchUpdateOp {
final Map<Account.Id, ChangeControl> reviewers;
+ final Collection<Address> reviewersByEmail;
final ReviewerState state;
final NotifyHandling notify;
final ListMultimap<RecipientType, Account.Id> accountsToNotify;
- List<PatchSetApproval> addedReviewers;
- Collection<Account.Id> addedCCs;
+ List<PatchSetApproval> addedReviewers = new ArrayList<>();
+ Collection<Account.Id> addedCCs = new ArrayList<>();
+ Collection<Address> addedCCsByEmail = new ArrayList<>();
private final ChangeResource rsrc;
private PatchSet patchSet;
@@ -374,11 +439,13 @@
Op(
ChangeResource rsrc,
Map<Account.Id, ChangeControl> reviewers,
+ Collection<Address> reviewersByEmail,
ReviewerState state,
NotifyHandling notify,
ListMultimap<RecipientType, Account.Id> accountsToNotify) {
this.rsrc = rsrc;
this.reviewers = reviewers;
+ this.reviewersByEmail = reviewersByEmail;
this.state = state;
this.notify = notify;
this.accountsToNotify = checkNotNull(accountsToNotify);
@@ -387,27 +454,34 @@
@Override
public boolean updateChange(ChangeContext ctx)
throws RestApiException, OrmException, IOException {
- if (migration.readChanges() && state == CC) {
- addedCCs =
- approvalsUtil.addCcs(
- ctx.getNotes(),
- ctx.getUpdate(ctx.getChange().currentPatchSetId()),
- reviewers.keySet());
- if (addedCCs.isEmpty()) {
- return false;
+ if (!reviewers.isEmpty()) {
+ if (migration.readChanges() && state == CC) {
+ addedCCs =
+ approvalsUtil.addCcs(
+ ctx.getNotes(),
+ ctx.getUpdate(ctx.getChange().currentPatchSetId()),
+ reviewers.keySet());
+ if (addedCCs.isEmpty()) {
+ return false;
+ }
+ } else {
+ addedReviewers =
+ approvalsUtil.addReviewers(
+ ctx.getDb(),
+ ctx.getNotes(),
+ ctx.getUpdate(ctx.getChange().currentPatchSetId()),
+ rsrc.getControl().getLabelTypes(),
+ rsrc.getChange(),
+ reviewers.keySet());
+ if (addedReviewers.isEmpty()) {
+ return false;
+ }
}
- } else {
- addedReviewers =
- approvalsUtil.addReviewers(
- ctx.getDb(),
- ctx.getNotes(),
- ctx.getUpdate(ctx.getChange().currentPatchSetId()),
- rsrc.getControl().getLabelTypes(),
- rsrc.getChange(),
- reviewers.keySet());
- if (addedReviewers.isEmpty()) {
- return false;
- }
+ }
+
+ for (Address a : reviewersByEmail) {
+ ctx.getUpdate(ctx.getChange().currentPatchSetId())
+ .putReviewerByEmail(a, ReviewerStateInternal.fromReviewerState(state));
}
patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes());
@@ -416,26 +490,19 @@
@Override
public void postUpdate(Context ctx) throws Exception {
- if (addedReviewers != null || addedCCs != null) {
- if (addedReviewers == null) {
- addedReviewers = new ArrayList<>();
- }
- if (addedCCs == null) {
- addedCCs = new ArrayList<>();
- }
- emailReviewers(
- rsrc.getChange(),
- Lists.transform(addedReviewers, r -> r.getAccountId()),
- addedCCs,
- notify,
- accountsToNotify);
- if (!addedReviewers.isEmpty()) {
- List<Account> reviewers =
- Lists.transform(
- addedReviewers, psa -> accountCache.get(psa.getAccountId()).getAccount());
- reviewerAdded.fire(
- rsrc.getChange(), patchSet, reviewers, ctx.getAccount(), ctx.getWhen());
- }
+ emailReviewers(
+ rsrc.getChange(),
+ Lists.transform(addedReviewers, r -> r.getAccountId()),
+ addedCCs == null ? ImmutableList.of() : addedCCs,
+ reviewersByEmail,
+ addedCCsByEmail,
+ notify,
+ accountsToNotify);
+ if (!addedReviewers.isEmpty()) {
+ List<Account> reviewers =
+ Lists.transform(
+ addedReviewers, psa -> accountCache.get(psa.getAccountId()).getAccount());
+ reviewerAdded.fire(rsrc.getChange(), patchSet, reviewers, ctx.getAccount(), ctx.getWhen());
}
}
}
@@ -444,9 +511,11 @@
Change change,
Collection<Account.Id> added,
Collection<Account.Id> copied,
+ Collection<Address> addedByEmail,
+ Collection<Address> copiedByEmail,
NotifyHandling notify,
ListMultimap<RecipientType, Account.Id> accountsToNotify) {
- if (added.isEmpty() && copied.isEmpty()) {
+ if (added.isEmpty() && copied.isEmpty() && addedByEmail.isEmpty() && copiedByEmail.isEmpty()) {
return;
}
@@ -466,7 +535,7 @@
toCopy.add(id);
}
}
- if (toMail.isEmpty() && toCopy.isEmpty()) {
+ if (toMail.isEmpty() && toCopy.isEmpty() && addedByEmail.isEmpty() && copiedByEmail.isEmpty()) {
return;
}
@@ -478,7 +547,9 @@
cm.setAccountsToNotify(accountsToNotify);
cm.setFrom(userId);
cm.addReviewers(toMail);
+ cm.addReviewersByEmail(addedByEmail);
cm.addExtraCC(toCopy);
+ cm.addExtraCCByEmail(copiedByEmail);
cm.send();
} catch (Exception err) {
log.error("Cannot send email to new reviewers of change " + change.getId(), err);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
index 0e72979..3acb93b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -96,7 +96,8 @@
if (in == null) {
in = new PublishChangeEditInput();
}
- editUtil.publish(edit.get(), in.notify, notifyUtil.resolveAccounts(in.notifyDetails));
+ editUtil.publish(
+ rsrc.getControl(), edit.get(), in.notify, notifyUtil.resolveAccounts(in.notifyDetails));
return Response.none();
}
}
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 e64abaa..714f677 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,6 +14,7 @@
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;
@@ -22,13 +23,17 @@
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;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.webui.UiAction;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.account.AccountsCollection;
import com.google.gerrit.server.change.PostReviewers.Addition;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.UpdateException;
import com.google.gwtorm.server.OrmException;
@@ -41,6 +46,7 @@
public class PutAssignee
implements RestModifyView<ChangeResource, AssigneeInput>, UiAction<ChangeResource> {
+ private final AccountsCollection accounts;
private final SetAssigneeOp.Factory assigneeFactory;
private final BatchUpdate.Factory batchUpdateFactory;
private final Provider<ReviewDb> db;
@@ -49,11 +55,13 @@
@Inject
PutAssignee(
+ AccountsCollection accounts,
SetAssigneeOp.Factory assigneeFactory,
BatchUpdate.Factory batchUpdateFactory,
Provider<ReviewDb> db,
PostReviewers postReviewers,
AccountLoader.Factory accountLoaderFactory) {
+ this.accounts = accounts;
this.assigneeFactory = assigneeFactory;
this.batchUpdateFactory = batchUpdateFactory;
this.db = db;
@@ -62,29 +70,40 @@
}
@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 (input.assignee == null || input.assignee.trim().isEmpty()) {
+ public AccountInfo apply(ChangeResource rsrc, AssigneeInput input)
+ throws RestApiException, UpdateException, OrmException, IOException,
+ PermissionBackendException {
+ rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
+
+ input.assignee = Strings.nullToEmpty(input.assignee).trim();
+ if (input.assignee.isEmpty()) {
throw new BadRequestException("missing assignee field");
}
+ IdentifiedUser assignee = accounts.parse(input.assignee);
+ if (!assignee.getAccount().isActive()) {
+ throw new UnprocessableEntityException(input.assignee + " is not active");
+ }
+ try {
+ rsrc.permissions().database(db).user(assignee).check(ChangePermission.READ);
+ } catch (AuthException e) {
+ throw new AuthException("read not permitted for " + input.assignee);
+ }
+
try (BatchUpdate bu =
batchUpdateFactory.create(
db.get(),
rsrc.getChange().getProject(),
rsrc.getControl().getUser(),
TimeUtil.nowTs())) {
- SetAssigneeOp op = assigneeFactory.create(input.assignee);
+ SetAssigneeOp op = assigneeFactory.create(assignee);
bu.addOp(rsrc.getId(), op);
PostReviewers.Addition reviewersAddition = addAssigneeAsCC(rsrc, input.assignee);
bu.addOp(rsrc.getId(), reviewersAddition.op);
bu.execute();
- return Response.ok(accountLoaderFactory.create(true).fillOne(op.getNewAssignee()));
+ return accountLoaderFactory.create(true).fillOne(assignee.getAccountId());
}
}
@@ -99,9 +118,9 @@
}
@Override
- public UiAction.Description getDescription(ChangeResource resource) {
+ public UiAction.Description getDescription(ChangeResource rsrc) {
return new UiAction.Description()
.setLabel("Edit Assignee")
- .setVisible(resource.getControl().canEditAssignee());
+ .setVisible(rsrc.permissions().testOrFalse(ChangePermission.EDIT_ASSIGNEE));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java
index 3c2633e..e872206 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java
@@ -16,7 +16,6 @@
import com.google.common.base.Strings;
import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -28,6 +27,8 @@
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
@@ -65,11 +66,10 @@
@Override
public Response<String> apply(RevisionResource rsrc, Input input)
- throws UpdateException, RestApiException {
+ throws UpdateException, RestApiException, PermissionBackendException {
+ rsrc.permissions().check(ChangePermission.EDIT_DESCRIPTION);
+
ChangeControl ctl = rsrc.getControl();
- if (!ctl.canEditDescription()) {
- throw new AuthException("changing description not permitted");
- }
Op op = new Op(input != null ? input : new Input(), rsrc.getPatchSet().getId());
try (BatchUpdate u =
batchUpdateFactory.create(
@@ -129,6 +129,6 @@
public UiAction.Description getDescription(RevisionResource rsrc) {
return new UiAction.Description()
.setLabel("Edit Description")
- .setVisible(rsrc.getControl().canEditDescription());
+ .setVisible(rsrc.permissions().testOrFalse(ChangePermission.EDIT_DESCRIPTION));
}
}
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 b289da8..ecdb382 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
@@ -146,7 +146,7 @@
update,
Status.DRAFT,
Collections.singleton(update(comment, in, ctx.getWhen())));
- ctx.bumpLastUpdatedOn(false);
+ ctx.dontBumpLastUpdatedOn();
return true;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java
new file mode 100644
index 0000000..6c9fadf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class PutPrivate
+ implements RestModifyView<ChangeResource, PutPrivate.Input>, UiAction<ChangeResource> {
+ public static class Input {}
+
+ private final Provider<ReviewDb> dbProvider;
+ private final BatchUpdate.Factory batchUpdateFactory;
+
+ @Inject
+ PutPrivate(Provider<ReviewDb> dbProvider, BatchUpdate.Factory batchUpdateFactory) {
+ this.dbProvider = dbProvider;
+ this.batchUpdateFactory = batchUpdateFactory;
+ }
+
+ @Override
+ public Response<String> apply(ChangeResource rsrc, Input input)
+ throws RestApiException, UpdateException {
+ if (!rsrc.getControl().isOwner()) {
+ throw new AuthException("not allowed to mark private");
+ }
+
+ if (rsrc.getChange().isPrivate()) {
+ return Response.ok("");
+ }
+
+ ChangeControl control = rsrc.getControl();
+ SetPrivateOp op = new SetPrivateOp(true);
+ try (BatchUpdate u =
+ batchUpdateFactory.create(
+ dbProvider.get(),
+ control.getProject().getNameKey(),
+ control.getUser(),
+ TimeUtil.nowTs())) {
+ u.addOp(control.getId(), op).execute();
+ }
+
+ return Response.created("");
+ }
+
+ @Override
+ public Description getDescription(ChangeResource rsrc) {
+ return new UiAction.Description()
+ .setLabel("Mark private")
+ .setTitle("Mark change as private")
+ .setVisible(rsrc.getControl().isOwner());
+ }
+}
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 783ab9d..b1e351b 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
@@ -16,7 +16,6 @@
import com.google.common.base.Strings;
import com.google.gerrit.common.TimeUtil;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -29,7 +28,8 @@
import com.google.gerrit.server.change.PutTopic.Input;
import com.google.gerrit.server.extensions.events.TopicEdited;
import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
import com.google.gerrit.server.update.ChangeContext;
@@ -65,16 +65,13 @@
@Override
public Response<String> apply(ChangeResource req, Input input)
- throws UpdateException, RestApiException {
- ChangeControl ctl = req.getControl();
- if (!ctl.canEditTopicName()) {
- throw new AuthException("changing topic not permitted");
- }
+ throws UpdateException, RestApiException, PermissionBackendException {
+ req.permissions().check(ChangePermission.EDIT_TOPIC_NAME);
Op op = new Op(input != null ? input : new Input());
try (BatchUpdate u =
batchUpdateFactory.create(
- dbProvider.get(), req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) {
+ dbProvider.get(), req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
u.addOp(req.getId(), op);
u.execute();
}
@@ -129,9 +126,9 @@
}
@Override
- public UiAction.Description getDescription(ChangeResource resource) {
+ public UiAction.Description getDescription(ChangeResource rsrc) {
return new UiAction.Description()
.setLabel("Edit Topic")
- .setVisible(resource.getControl().canEditTopicName());
+ .setVisible(rsrc.permissions().testOrFalse(ChangePermission.EDIT_TOPIC_NAME));
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
index 93e1e4e..4f8646d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -35,6 +35,8 @@
import com.google.gerrit.server.change.RebaseUtil.Base;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.update.BatchUpdate;
@@ -85,7 +87,9 @@
@Override
public ChangeInfo apply(RevisionResource rsrc, RebaseInput input)
throws EmailException, OrmException, UpdateException, RestApiException, IOException,
- NoSuchChangeException {
+ NoSuchChangeException, PermissionBackendException {
+ rsrc.permissions().database(dbProvider).check(ChangePermission.REBASE);
+
ChangeControl control = rsrc.getControl();
Change change = rsrc.getChange();
try (Repository repo = repoManager.openRepository(change.getProject());
@@ -94,9 +98,7 @@
BatchUpdate bu =
updateFactory.create(
dbProvider.get(), change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
- if (!control.canRebase(dbProvider.get())) {
- throw new AuthException("rebase not permitted");
- } else if (!change.getStatus().isOpen()) {
+ if (!change.getStatus().isOpen()) {
throw new ResourceConflictException("change is " + change.getStatus().name().toLowerCase());
} else if (!hasOneParent(rw, rsrc.getPatchSet())) {
throw new ResourceConflictException(
@@ -175,15 +177,12 @@
@Override
public UiAction.Description getDescription(RevisionResource resource) {
PatchSet patchSet = resource.getPatchSet();
- Branch.NameKey dest = resource.getChange().getDest();
- boolean canRebase = false;
- try {
- canRebase = resource.getControl().canRebase(dbProvider.get());
- } catch (OrmException e) {
- log.error("Cannot check canRebase status. Assuming false.", e);
- }
+ Change change = resource.getChange();
+ Branch.NameKey dest = change.getDest();
boolean visible =
- resource.getChange().getStatus().isOpen() && resource.isCurrent() && canRebase;
+ change.getStatus().isOpen()
+ && resource.isCurrent()
+ && resource.permissions().database(dbProvider).testOrFalse(ChangePermission.REBASE);
boolean enabled = true;
if (visible) {
@@ -196,13 +195,11 @@
visible = false;
}
}
- UiAction.Description descr =
- new UiAction.Description()
- .setLabel("Rebase")
- .setTitle("Rebase onto tip of branch or parent change")
- .setVisible(visible)
- .setEnabled(enabled);
- return descr;
+ return new UiAction.Description()
+ .setLabel("Rebase")
+ .setTitle("Rebase onto tip of branch or parent change")
+ .setVisible(visible)
+ .setEnabled(enabled);
}
public static class CurrentRevision implements RestModifyView<ChangeResource, RebaseInput> {
@@ -217,7 +214,8 @@
@Override
public ChangeInfo apply(ChangeResource rsrc, RebaseInput input)
- throws EmailException, OrmException, UpdateException, RestApiException, IOException {
+ throws EmailException, OrmException, UpdateException, RestApiException, IOException,
+ PermissionBackendException {
PatchSet ps = psUtil.current(rebase.dbProvider.get(), rsrc.getNotes());
if (ps == null) {
throw new ResourceConflictException("current revision is missing");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
index c03bb6f..7bd4bc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -36,8 +36,8 @@
import com.google.gerrit.server.update.Context;
import com.google.gerrit.server.update.RepoContext;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
@@ -75,7 +75,7 @@
private PatchSetInserter patchSetInserter;
private PatchSet rebasedPatchSet;
- @AssistedInject
+ @Inject
RebaseChangeOp(
PatchSetInserter.Factory patchSetInserterFactory,
MergeUtil.Factory mergeUtilFactory,
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 b6c4d02..aa3f56d 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
@@ -18,7 +18,6 @@
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.changes.RestoreInput;
import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -34,6 +33,8 @@
import com.google.gerrit.server.mail.send.ReplyToChangeSender;
import com.google.gerrit.server.mail.send.RestoredSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.BatchUpdateOp;
@@ -80,12 +81,10 @@
@Override
public ChangeInfo apply(ChangeResource req, RestoreInput input)
- throws RestApiException, UpdateException, OrmException {
- ChangeControl ctl = req.getControl();
- if (!ctl.canRestore(dbProvider.get())) {
- throw new AuthException("restore not permitted");
- }
+ throws RestApiException, UpdateException, OrmException, PermissionBackendException {
+ req.permissions().database(dbProvider).check(ChangePermission.RESTORE);
+ ChangeControl ctl = req.getControl();
Op op = new Op(input);
try (BatchUpdate u =
batchUpdateFactory.create(
@@ -150,17 +149,13 @@
}
@Override
- public UiAction.Description getDescription(ChangeResource resource) {
- boolean canRestore = false;
- try {
- canRestore = resource.getControl().canRestore(dbProvider.get());
- } catch (OrmException e) {
- log.error("Cannot check canRestore status. Assuming false.", e);
- }
+ public UiAction.Description getDescription(ChangeResource rsrc) {
return new UiAction.Description()
.setLabel("Restore")
.setTitle("Restore the change")
- .setVisible(resource.getChange().getStatus() == Status.ABANDONED && canRestore);
+ .setVisible(
+ rsrc.getChange().getStatus() == Status.ABANDONED
+ && rsrc.permissions().database(dbProvider).testOrFalse(ChangePermission.RESTORE));
}
private static String status(Change change) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
index 6ff4a50..f6f7919 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -14,11 +14,15 @@
package com.google.gerrit.server.change;
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.TypeLiteral;
import com.google.inject.assistedinject.Assisted;
@@ -36,7 +40,8 @@
private final ChangeResource change;
private final RevisionResource revision;
- private final IdentifiedUser user;
+ @Nullable private final IdentifiedUser user;
+ @Nullable private final Address address;
@AssistedInject
ReviewerResource(
@@ -44,8 +49,9 @@
@Assisted ChangeResource change,
@Assisted Account.Id id) {
this.change = change;
- this.revision = null;
this.user = userFactory.create(id);
+ this.revision = null;
+ this.address = null;
}
@AssistedInject
@@ -56,6 +62,21 @@
this.revision = revision;
this.change = revision.getChangeResource();
this.user = userFactory.create(id);
+ this.address = null;
+ }
+
+ ReviewerResource(ChangeResource change, Address address) {
+ this.change = change;
+ this.address = address;
+ this.revision = null;
+ this.user = null;
+ }
+
+ ReviewerResource(RevisionResource revision, Address address) {
+ this.revision = revision;
+ this.change = revision.getChangeResource();
+ this.address = address;
+ this.user = null;
}
public ChangeResource getChangeResource() {
@@ -75,10 +96,28 @@
}
public IdentifiedUser getReviewerUser() {
+ checkArgument(user != null, "no user provided");
return user;
}
+ public Address getReviewerByEmail() {
+ checkArgument(address != null, "no address provided");
+ return address;
+ }
+
/**
+ * Check if this resource was constructed by email or by {@code Account.Id}.
+ *
+ * @return true if the resource was constructed by providing an {@code Address}; false if the
+ * resource was constructed by providing an {@code Account.Id}.
+ */
+ public boolean isByEmail() {
+ return user == null;
+ }
+
+ /**
+ * Get the control for the caller's user.
+ *
* @return the control for the caller's user (as opposed to the reviewer's user as returned by
* {@link #getReviewerControl()}).
*/
@@ -87,10 +126,13 @@
}
/**
+ * Get the control for the reviewer's user.
+ *
* @return the control for the reviewer's user (as opposed to the caller's user as returned by
* {@link #getControl()}).
*/
public ChangeControl getReviewerControl() {
+ checkArgument(user != null, "no user provided");
return change.getControl().forUser(user);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
index 14c74bc..0762f0e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
@@ -25,6 +25,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.account.AccountsCollection;
+import com.google.gerrit.server.mail.Address;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -69,12 +70,26 @@
@Override
public ReviewerResource parse(ChangeResource rsrc, IdString id)
throws OrmException, ResourceNotFoundException, AuthException {
- Account.Id accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId();
+ Address address = Address.tryParse(id.get());
+ Account.Id accountId = null;
+ try {
+ accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId();
+ } catch (ResourceNotFoundException e) {
+ if (address == null) {
+ throw e;
+ }
+ }
// See if the id exists as a reviewer for this change
- if (fetchAccountIds(rsrc).contains(accountId)) {
+ if (accountId != null && fetchAccountIds(rsrc).contains(accountId)) {
return resourceFactory.create(rsrc, accountId);
}
+
+ // See if the address exists as a reviewer on the change
+ if (address != null && rsrc.getNotes().getReviewersByEmail().all().contains(address)) {
+ return new ReviewerResource(rsrc, address);
+ }
+
throw new ResourceNotFoundException(id);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
index 4d35f9e..32132bc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
@@ -24,6 +24,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.edit.ChangeEdit;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.TypeLiteral;
import java.util.Optional;
@@ -51,6 +52,10 @@
return cacheable;
}
+ public PermissionBackend.ForChange permissions() {
+ return change.permissions();
+ }
+
public ChangeResource getChangeResource() {
return change;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java
index d3623cf..2dc7ad8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java
@@ -26,6 +26,7 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.account.AccountsCollection;
+import com.google.gerrit.server.mail.Address;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -73,14 +74,28 @@
if (!rsrc.isCurrent()) {
throw new MethodNotAllowedException("Cannot access on non-current patch set");
}
+ Address address = Address.tryParse(id.get());
- Account.Id accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId();
-
+ Account.Id accountId = null;
+ try {
+ accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId();
+ } catch (ResourceNotFoundException e) {
+ if (address == null) {
+ throw e;
+ }
+ }
Collection<Account.Id> reviewers =
approvalsUtil.getReviewers(dbProvider.get(), rsrc.getNotes()).all();
+ // See if the id exists as a reviewer for this change
if (reviewers.contains(accountId)) {
return resourceFactory.create(rsrc, accountId);
}
+
+ // See if the address exists as a reviewer on the change
+ if (address != null && rsrc.getNotes().getReviewersByEmail().all().contains(address)) {
+ return new ReviewerResource(rsrc, address);
+ }
+
throw new ResourceNotFoundException(id);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
index a16f2f9..5ecb904 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
@@ -37,6 +37,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
@Singleton
public class Revisions implements ChildCollection<ChangeResource, RevisionResource> {
@@ -143,8 +144,9 @@
Optional<ChangeEdit> edit = editUtil.byChange(change.getChange());
if (edit.isPresent()) {
PatchSet ps = new PatchSet(new PatchSet.Id(change.getId(), 0));
- ps.setRevision(edit.get().getRevision());
- if (revid == null || edit.get().getRevision().equals(revid)) {
+ RevId editRevId = new RevId(ObjectId.toString(edit.get().getEditCommit()));
+ ps.setRevision(editRevId);
+ if (revid == null || editRevId.equals(revid)) {
return Collections.singletonList(new RevisionResource(change, ps, edit));
}
}
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 409be9d..73a6c60 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
@@ -17,16 +17,12 @@
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.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.IdentifiedUser;
-import com.google.gerrit.server.account.AccountsCollection;
import com.google.gerrit.server.extensions.events.AssigneeChanged;
import com.google.gerrit.server.mail.send.SetAssigneeSender;
import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -36,9 +32,9 @@
import com.google.gerrit.server.validators.AssigneeValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,93 +42,74 @@
private static final Logger log = LoggerFactory.getLogger(SetAssigneeOp.class);
public interface Factory {
- SetAssigneeOp create(String assignee);
+ SetAssigneeOp create(IdentifiedUser assignee);
}
- private final AccountsCollection accounts;
private final ChangeMessagesUtil cmUtil;
private final DynamicSet<AssigneeValidationListener> validationListeners;
- private final String assignee;
+ private final IdentifiedUser newAssignee;
private final AssigneeChanged assigneeChanged;
private final SetAssigneeSender.Factory setAssigneeSenderFactory;
private final Provider<IdentifiedUser> user;
private final IdentifiedUser.GenericFactory userFactory;
private Change change;
- private Account newAssignee;
- private Account oldAssignee;
+ private IdentifiedUser oldAssignee;
- @AssistedInject
+ @Inject
SetAssigneeOp(
- AccountsCollection accounts,
ChangeMessagesUtil cmUtil,
DynamicSet<AssigneeValidationListener> validationListeners,
AssigneeChanged assigneeChanged,
SetAssigneeSender.Factory setAssigneeSenderFactory,
Provider<IdentifiedUser> user,
IdentifiedUser.GenericFactory userFactory,
- @Assisted String assignee) {
- this.accounts = accounts;
+ @Assisted IdentifiedUser newAssignee) {
this.cmUtil = cmUtil;
this.validationListeners = validationListeners;
this.assigneeChanged = assigneeChanged;
this.setAssigneeSenderFactory = setAssigneeSenderFactory;
this.user = user;
this.userFactory = userFactory;
- this.assignee = checkNotNull(assignee);
+ this.newAssignee = checkNotNull(newAssignee, "assignee");
}
@Override
public boolean updateChange(ChangeContext ctx) throws OrmException, RestApiException {
change = ctx.getChange();
- ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
- IdentifiedUser newAssigneeUser = accounts.parse(assignee);
- newAssignee = newAssigneeUser.getAccount();
- IdentifiedUser oldAssigneeUser = null;
- if (change.getAssignee() != null) {
- oldAssigneeUser = userFactory.create(change.getAssignee());
- oldAssignee = oldAssigneeUser.getAccount();
- if (newAssignee.equals(oldAssignee)) {
- return false;
- }
- }
- if (!newAssignee.isActive()) {
- throw new UnprocessableEntityException(
- String.format("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(), assignee));
+ if (newAssignee.getAccountId().equals(change.getAssignee())) {
+ return false;
}
try {
for (AssigneeValidationListener validator : validationListeners) {
- validator.validateAssignee(change, newAssignee);
+ validator.validateAssignee(change, newAssignee.getAccount());
}
} catch (ValidationException e) {
throw new ResourceConflictException(e.getMessage());
}
+
+ if (change.getAssignee() != null) {
+ oldAssignee = userFactory.create(change.getAssignee());
+ }
+
+ ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
// notedb
- update.setAssignee(newAssignee.getId());
+ update.setAssignee(newAssignee.getAccountId());
// reviewdb
- change.setAssignee(newAssignee.getId());
- addMessage(ctx, update, oldAssigneeUser, newAssigneeUser);
+ change.setAssignee(newAssignee.getAccountId());
+ addMessage(ctx, update);
return true;
}
- private void addMessage(
- ChangeContext ctx,
- ChangeUpdate update,
- IdentifiedUser previousAssignee,
- IdentifiedUser newAssignee)
- throws OrmException {
+ private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException {
StringBuilder msg = new StringBuilder();
msg.append("Assignee ");
- if (previousAssignee == null) {
+ if (oldAssignee == null) {
msg.append("added: ");
msg.append(newAssignee.getNameEmail());
} else {
msg.append("changed from: ");
- msg.append(previousAssignee.getNameEmail());
+ msg.append(oldAssignee.getNameEmail());
msg.append(" to: ");
msg.append(newAssignee.getNameEmail());
}
@@ -145,16 +122,17 @@
public void postUpdate(Context ctx) throws OrmException {
try {
SetAssigneeSender cm =
- setAssigneeSenderFactory.create(change.getProject(), change.getId(), newAssignee.getId());
+ setAssigneeSenderFactory.create(
+ change.getProject(), change.getId(), newAssignee.getAccountId());
cm.setFrom(user.get().getAccountId());
cm.send();
} catch (Exception err) {
log.error("Cannot send email to new assignee of change " + change.getId(), err);
}
- assigneeChanged.fire(change, ctx.getAccount(), oldAssignee, ctx.getWhen());
- }
-
- public Account.Id getNewAssignee() {
- return newAssignee != null ? newAssignee.getId() : null;
+ assigneeChanged.fire(
+ change,
+ ctx.getAccount(),
+ oldAssignee != null ? oldAssignee.getAccount() : null,
+ ctx.getWhen());
}
}
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 0e78c18..724598a 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
@@ -39,8 +39,8 @@
import com.google.gerrit.server.validators.HashtagValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
@@ -64,7 +64,7 @@
private Set<String> toRemove;
private ImmutableSortedSet<String> updatedHashtags;
- @AssistedInject
+ @Inject
SetHashtagsOp(
NotesMigration notesMigration,
ChangeMessagesUtil cmUtil,
@@ -94,9 +94,7 @@
updatedHashtags = ImmutableSortedSet.of();
return false;
}
- if (!ctx.getControl().canEditHashtags()) {
- throw new AuthException("Editing hashtags not permitted");
- }
+
change = ctx.getChange();
ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
ChangeNotes notes = update.getNotes().load();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java
new file mode 100644
index 0000000..dde4a9d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+
+class SetPrivateOp implements BatchUpdateOp {
+ private final boolean isPrivate;
+
+ SetPrivateOp(boolean isPrivate) {
+ this.isPrivate = isPrivate;
+ }
+
+ @Override
+ public boolean updateChange(ChangeContext ctx) {
+ Change change = ctx.getChange();
+ ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId());
+ change.setPrivate(isPrivate);
+ change.setLastUpdatedOn(ctx.getWhen());
+ update.setPrivate(isPrivate);
+ return true;
+ }
+}
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 01020fa..b996133 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
@@ -51,7 +51,9 @@
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MergeSuperSet;
import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
@@ -62,6 +64,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -122,13 +125,13 @@
private final Provider<ReviewDb> dbProvider;
private final GitRepositoryManager repoManager;
+ private final PermissionBackend permissionBackend;
private final ChangeData.Factory changeDataFactory;
private final ChangeMessagesUtil cmUtil;
private final ChangeNotes.Factory changeNotesFactory;
private final Provider<MergeOp> mergeOpProvider;
private final Provider<MergeSuperSet> mergeSuperSet;
private final AccountsCollection accounts;
- private final ChangesCollection changes;
private final String label;
private final String labelWithParents;
private final ParameterizedString titlePattern;
@@ -143,25 +146,25 @@
Submit(
Provider<ReviewDb> dbProvider,
GitRepositoryManager repoManager,
+ PermissionBackend permissionBackend,
ChangeData.Factory changeDataFactory,
ChangeMessagesUtil cmUtil,
ChangeNotes.Factory changeNotesFactory,
Provider<MergeOp> mergeOpProvider,
Provider<MergeSuperSet> mergeSuperSet,
AccountsCollection accounts,
- ChangesCollection changes,
@GerritServerConfig Config cfg,
Provider<InternalChangeQuery> queryProvider,
PatchSetUtil psUtil) {
this.dbProvider = dbProvider;
this.repoManager = repoManager;
+ this.permissionBackend = permissionBackend;
this.changeDataFactory = changeDataFactory;
this.cmUtil = cmUtil;
this.changeNotesFactory = changeNotesFactory;
this.mergeOpProvider = mergeOpProvider;
this.mergeSuperSet = mergeSuperSet;
this.accounts = accounts;
- this.changes = changes;
this.label =
MoreObjects.firstNonNull(
Strings.emptyToNull(cfg.getString("change", null, "submitLabel")), "Submit");
@@ -193,17 +196,19 @@
@Override
public Output apply(RevisionResource rsrc, SubmitInput input)
- throws RestApiException, RepositoryNotFoundException, IOException, OrmException {
+ throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
+ PermissionBackendException {
input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf);
+ IdentifiedUser submitter;
if (input.onBehalfOf != null) {
- rsrc = onBehalfOf(rsrc, input);
+ submitter = onBehalfOf(rsrc, input);
+ } else {
+ rsrc.permissions().check(ChangePermission.SUBMIT);
+ submitter = rsrc.getUser().asIdentifiedUser();
}
- ChangeControl control = rsrc.getControl();
- IdentifiedUser caller = control.getUser().asIdentifiedUser();
+
Change change = rsrc.getChange();
- if (input.onBehalfOf == null && !control.canSubmit()) {
- throw new AuthException("submit not permitted");
- } else if (!change.getStatus().isOpen()) {
+ if (!change.getStatus().isOpen()) {
throw new ResourceConflictException("change is " + status(change));
} else if (!ProjectUtil.branchExists(repoManager, change.getDest())) {
throw new ResourceConflictException(
@@ -217,7 +222,7 @@
try (MergeOp op = mergeOpProvider.get()) {
ReviewDb db = dbProvider.get();
- op.merge(db, change, caller, true, input, false);
+ op.merge(db, change, submitter, true, input, false);
try {
change =
changeNotesFactory.createChecked(db, change.getProject(), change.getId()).getChange();
@@ -250,18 +255,20 @@
*/
private String problemsForSubmittingChangeset(ChangeData cd, ChangeSet cs, CurrentUser user) {
try {
- @SuppressWarnings("resource")
- ReviewDb db = dbProvider.get();
if (cs.furtherHiddenChanges()) {
return BLOCKED_HIDDEN_SUBMIT_TOOLTIP;
}
for (ChangeData c : cs.changes()) {
- ChangeControl changeControl = c.changeControl(user);
-
- if (!changeControl.isVisible(db)) {
+ Set<ChangePermission> can =
+ permissionBackend
+ .user(user)
+ .database(dbProvider)
+ .change(c)
+ .test(EnumSet.of(ChangePermission.READ, ChangePermission.SUBMIT));
+ if (!can.contains(ChangePermission.READ)) {
return BLOCKED_HIDDEN_SUBMIT_TOOLTIP;
}
- if (!changeControl.canSubmit()) {
+ if (!can.contains(ChangePermission.SUBMIT)) {
return BLOCKED_SUBMIT_TOOLTIP;
}
MergeOp.checkSubmitRule(c);
@@ -281,7 +288,7 @@
}
} catch (ResourceConflictException e) {
return BLOCKED_SUBMIT_TOOLTIP;
- } catch (OrmException | IOException e) {
+ } catch (PermissionBackendException | OrmException | IOException e) {
log.error("Error checking if change is submittable", e);
throw new OrmRuntimeException("Could not determine problems for the change", e);
}
@@ -290,20 +297,23 @@
@Override
public UiAction.Description getDescription(RevisionResource resource) {
- PatchSet.Id current = resource.getChange().currentPatchSetId();
- String topic = resource.getChange().getTopic();
- boolean visible =
- !resource.getPatchSet().isDraft()
- && resource.getChange().getStatus().isOpen()
- && resource.getPatchSet().getId().equals(current)
- && resource.getControl().canSubmit();
+ Change change = resource.getChange();
+ String topic = change.getTopic();
ReviewDb db = dbProvider.get();
ChangeData cd = changeDataFactory.create(db, resource.getControl());
-
+ boolean visible;
try {
+ visible =
+ change.getStatus().isOpen()
+ && resource.isCurrent()
+ && !resource.getPatchSet().isDraft()
+ && resource.permissions().test(ChangePermission.SUBMIT);
MergeOp.checkSubmitRule(cd);
} catch (ResourceConflictException e) {
visible = false;
+ } catch (PermissionBackendException e) {
+ log.error("Error checking if change is submittable", e);
+ throw new OrmRuntimeException("Could not check submit permission", e);
} catch (OrmException e) {
log.error("Error checking if change is submittable", e);
throw new OrmRuntimeException("Could not determine problems for the change", e);
@@ -367,7 +377,7 @@
Map<String, String> params =
ImmutableMap.of(
"patchSet", String.valueOf(resource.getPatchSet().getPatchSetId()),
- "branch", resource.getChange().getDest().getShortName(),
+ "branch", change.getDest().getShortName(),
"commit", ObjectId.fromString(revId.get()).abbreviate(7).name(),
"submitSize", String.valueOf(cs.size()));
ParameterizedString tp = cs.size() > 1 ? titlePatternWithAncestors : titlePattern;
@@ -458,24 +468,21 @@
return commits;
}
- private RevisionResource onBehalfOf(RevisionResource rsrc, SubmitInput in)
- throws AuthException, UnprocessableEntityException, OrmException {
- ChangeControl caller = rsrc.getControl();
- if (!caller.canSubmit()) {
- throw new AuthException("submit not permitted");
- }
- if (!caller.canSubmitAs()) {
- throw new AuthException("submit on behalf of not permitted");
- }
- ChangeControl target =
- caller.forUser(accounts.parseOnBehalfOf(caller.getUser(), in.onBehalfOf));
- if (!target.getRefControl().isVisible()) {
+ private IdentifiedUser onBehalfOf(RevisionResource rsrc, SubmitInput in)
+ throws AuthException, UnprocessableEntityException, OrmException, PermissionBackendException {
+ PermissionBackend.ForChange perm = rsrc.permissions().database(dbProvider);
+ perm.check(ChangePermission.SUBMIT);
+ perm.check(ChangePermission.SUBMIT_AS);
+
+ CurrentUser caller = rsrc.getUser();
+ IdentifiedUser submitter = accounts.parseOnBehalfOf(caller, in.onBehalfOf);
+ try {
+ perm.user(submitter).check(ChangePermission.READ);
+ } catch (AuthException e) {
throw new UnprocessableEntityException(
- String.format(
- "on_behalf_of account %s cannot see destination ref",
- target.getUser().getAccountId()));
+ String.format("on_behalf_of account %s cannot see change", submitter.getAccountId()));
}
- return new RevisionResource(changes.parse(target), rsrc.getPatchSet());
+ return submitter;
}
public static boolean wholeTopicEnabled(Config config) {
@@ -510,7 +517,8 @@
@Override
public ChangeInfo apply(ChangeResource rsrc, SubmitInput input)
- throws RestApiException, RepositoryNotFoundException, IOException, OrmException {
+ throws RestApiException, RepositoryNotFoundException, IOException, OrmException,
+ PermissionBackendException {
PatchSet ps = psUtil.current(dbProvider.get(), rsrc.getNotes());
if (ps == null) {
throw new ResourceConflictException("current revision is missing");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index 6cdb5e56..7b93277 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -14,13 +14,13 @@
package com.google.gerrit.server.config;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_UUID;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID;
import com.google.gerrit.extensions.client.AuthType;
import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
import com.google.gwtjsonrpc.server.SignedToken;
import com.google.gwtjsonrpc.server.XsrfException;
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 7b110e3..5f70786 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
@@ -92,6 +92,7 @@
import com.google.gerrit.server.account.GroupIncludeCacheImpl;
import com.google.gerrit.server.account.GroupMembers;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
+import com.google.gerrit.server.account.externalids.ExternalIdModule;
import com.google.gerrit.server.api.accounts.AccountExternalIdCreator;
import com.google.gerrit.server.auth.AuthBackend;
import com.google.gerrit.server.auth.UniversalAuthBackend;
@@ -156,11 +157,10 @@
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.project.AccessControlModule;
-import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.CommentLinkProvider;
+import com.google.gerrit.server.project.DefaultPermissionBackendModule;
import com.google.gerrit.server.project.PermissionCollection;
import com.google.gerrit.server.project.ProjectCacheImpl;
-import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectNode;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SectionSortCache;
@@ -227,7 +227,9 @@
install(new AccessControlModule());
install(new CmdLineParserModule());
+ install(new DefaultPermissionBackendModule());
install(new EmailModule());
+ install(new ExternalIdModule());
install(new GitModule());
install(new GroupModule());
install(new NoteDbModule(cfg));
@@ -287,8 +289,6 @@
bind(PatchSetInfoFactory.class);
bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
- bind(ChangeControl.GenericFactory.class);
- bind(ProjectControl.GenericFactory.class);
bind(AccountControl.Factory.class);
install(new AuditModule());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
index 3907c98..925cf64 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -309,9 +309,15 @@
PluginConfigInfo info = new PluginConfigInfo();
info.hasAvatars = toBoolean(avatar.get() != null);
info.jsResourcePaths = new ArrayList<>();
+ info.htmlResourcePaths = new ArrayList<>();
for (WebUiPlugin u : plugins) {
- info.jsResourcePaths.add(
- String.format("plugins/%s/%s", u.getPluginName(), u.getJavaScriptResourcePath()));
+ String path =
+ String.format("plugins/%s/%s", u.getPluginName(), u.getJavaScriptResourcePath());
+ if (path.endsWith(".html")) {
+ info.htmlResourcePaths.add(path);
+ } else {
+ info.jsResourcePaths.add(path);
+ }
}
return info;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index 33e68d3..a2e0356 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -18,8 +18,8 @@
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.util.ServerRequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
/**
* Provider of the group(s) which should become owners of a newly created project. The only matching
@@ -40,7 +40,7 @@
ProjectOwnerGroupsProvider create(Project.NameKey project);
}
- @AssistedInject
+ @Inject
public ProjectOwnerGroupsProvider(
GroupBackend gb,
ThreadLocalRequestContext context,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
index 3987aed..4358186 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
@@ -41,7 +41,7 @@
@Override
public ReviewDb get() {
if (db == null) {
- final ReviewDb c;
+ ReviewDb c;
try {
c = schema.open();
} catch (OrmException e) {
@@ -51,12 +51,9 @@
cleanup
.get()
.add(
- new Runnable() {
- @Override
- public void run() {
- c.close();
- db = null;
- }
+ () -> {
+ c.close();
+ db = null;
});
} catch (Throwable e) {
c.close();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEdit.java
index a6464a7..e641abc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEdit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEdit.java
@@ -18,11 +18,6 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.server.IdentifiedUser;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
/**
@@ -33,44 +28,25 @@
* change number and P is the patch set number it is based on.
*/
public class ChangeEdit {
- private final IdentifiedUser user;
private final Change change;
- private final Ref ref;
+ private final String editRefName;
private final RevCommit editCommit;
private final PatchSet basePatchSet;
public ChangeEdit(
- IdentifiedUser user, Change change, Ref ref, RevCommit editCommit, PatchSet basePatchSet) {
- checkNotNull(user);
- checkNotNull(change);
- checkNotNull(ref);
- checkNotNull(editCommit);
- checkNotNull(basePatchSet);
- this.user = user;
- this.change = change;
- this.ref = ref;
- this.editCommit = editCommit;
- this.basePatchSet = basePatchSet;
+ Change change, String editRefName, RevCommit editCommit, PatchSet basePatchSet) {
+ this.change = checkNotNull(change);
+ this.editRefName = checkNotNull(editRefName);
+ this.editCommit = checkNotNull(editCommit);
+ this.basePatchSet = checkNotNull(basePatchSet);
}
public Change getChange() {
return change;
}
- public IdentifiedUser getUser() {
- return user;
- }
-
- public Ref getRef() {
- return ref;
- }
-
- public RevId getRevision() {
- return new RevId(ObjectId.toString(ref.getObjectId()));
- }
-
public String getRefName() {
- return RefNames.refsEdit(user.getAccountId(), change.getId(), basePatchSet.getId());
+ return editRefName;
}
public RevCommit getEditCommit() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 3d1fb79..75da89d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
@@ -44,6 +45,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.sql.Timestamp;
+import java.util.List;
import java.util.Optional;
import java.util.TimeZone;
import org.eclipse.jgit.lib.BatchRefUpdate;
@@ -116,8 +118,7 @@
PatchSet currentPatchSet = lookupCurrentPatchSet(changeControl);
ObjectId patchSetCommitId = getPatchSetCommitId(currentPatchSet);
- createEditReference(
- repository, changeControl, currentPatchSet, patchSetCommitId, TimeUtil.nowTs());
+ createEdit(repository, changeControl, currentPatchSet, patchSetCommitId, TimeUtil.nowTs());
}
/**
@@ -218,9 +219,9 @@
createCommit(repository, basePatchSetCommit, baseTree, newCommitMessage, nowTimestamp);
if (optionalChangeEdit.isPresent()) {
- updateEditReference(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp);
+ updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp);
} else {
- createEditReference(repository, changeControl, basePatchSet, newEditCommit, nowTimestamp);
+ createEdit(repository, changeControl, basePatchSet, newEditCommit, nowTimestamp);
}
}
@@ -308,7 +309,7 @@
RevCommit baseCommit =
optionalChangeEdit.map(ChangeEdit::getEditCommit).orElse(basePatchSetCommit);
- ObjectId newTreeId = createNewTree(repository, baseCommit, treeModification);
+ ObjectId newTreeId = createNewTree(repository, baseCommit, ImmutableList.of(treeModification));
String commitMessage = baseCommit.getFullMessage();
Timestamp nowTimestamp = TimeUtil.nowTs();
@@ -316,12 +317,64 @@
createCommit(repository, basePatchSetCommit, newTreeId, commitMessage, nowTimestamp);
if (optionalChangeEdit.isPresent()) {
- updateEditReference(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp);
+ updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp);
} else {
- createEditReference(repository, changeControl, basePatchSet, newEditCommit, nowTimestamp);
+ createEdit(repository, changeControl, basePatchSet, newEditCommit, nowTimestamp);
}
}
+ /**
+ * Applies the indicated modifications to the specified patch set. If a change edit exists and is
+ * based on the same patch set, the modified patch set tree is merged with the change edit. If the
+ * change edit doesn't exist, a new one will be created.
+ *
+ * @param repository the affected Git repository
+ * @param changeControl the {@code ChangeControl} of the change to which the patch set belongs
+ * @param patchSet the {@code PatchSet} which should be modified
+ * @param treeModifications the modifications which should be applied
+ * @return the resulting {@code ChangeEdit}
+ * @throws AuthException if the user isn't authenticated or not allowed to use change edits
+ * @throws InvalidChangeOperationException if the existing change edit is based on another patch
+ * set or no change edit exists but the specified patch set isn't the current one
+ * @throws MergeConflictException if the modified patch set tree can't be merged with an existing
+ * change edit
+ */
+ public ChangeEdit combineWithModifiedPatchSetTree(
+ Repository repository,
+ ChangeControl changeControl,
+ PatchSet patchSet,
+ List<TreeModification> treeModifications)
+ throws AuthException, IOException, InvalidChangeOperationException, MergeConflictException,
+ OrmException {
+ ensureAuthenticatedAndPermitted(changeControl);
+
+ Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl);
+ ensureAllowedPatchSet(changeControl, optionalChangeEdit, patchSet);
+
+ RevCommit patchSetCommit = lookupCommit(repository, patchSet);
+ ObjectId newTreeId = createNewTree(repository, patchSetCommit, treeModifications);
+
+ if (optionalChangeEdit.isPresent()) {
+ ChangeEdit changeEdit = optionalChangeEdit.get();
+ newTreeId = merge(repository, changeEdit, newTreeId);
+ if (ObjectId.equals(newTreeId, changeEdit.getEditCommit().getTree())) {
+ // Modifications are already contained in the change edit.
+ return changeEdit;
+ }
+ }
+
+ String commitMessage =
+ optionalChangeEdit.map(ChangeEdit::getEditCommit).orElse(patchSetCommit).getFullMessage();
+ Timestamp nowTimestamp = TimeUtil.nowTs();
+ ObjectId newEditCommit =
+ createCommit(repository, patchSetCommit, newTreeId, commitMessage, nowTimestamp);
+
+ if (optionalChangeEdit.isPresent()) {
+ return updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp);
+ }
+ return createEdit(repository, changeControl, patchSet, newEditCommit, nowTimestamp);
+ }
+
private void ensureAuthenticatedAndPermitted(ChangeControl changeControl)
throws AuthException, OrmException {
ensureAuthenticated();
@@ -340,7 +393,31 @@
}
}
- private String getWellFormedCommitMessage(String commitMessage) {
+ private static void ensureAllowedPatchSet(
+ ChangeControl changeControl, Optional<ChangeEdit> optionalChangeEdit, PatchSet patchSet)
+ throws InvalidChangeOperationException {
+ if (optionalChangeEdit.isPresent()) {
+ ChangeEdit changeEdit = optionalChangeEdit.get();
+ if (!isBasedOn(changeEdit, patchSet)) {
+ throw new InvalidChangeOperationException(
+ String.format(
+ "Only the patch set %s on which the existing change edit is based may be modified "
+ + "(specified patch set: %s)",
+ changeEdit.getBasePatchSet().getId(), patchSet.getId()));
+ }
+ } else {
+ PatchSet.Id patchSetId = patchSet.getId();
+ PatchSet.Id currentPatchSetId = changeControl.getChange().currentPatchSetId();
+ if (!patchSetId.equals(currentPatchSetId)) {
+ throw new InvalidChangeOperationException(
+ String.format(
+ "A change edit may only be created for the current patch set %s (and not for %s)",
+ currentPatchSetId, patchSetId));
+ }
+ }
+ }
+
+ private static String getWellFormedCommitMessage(String commitMessage) {
String wellFormedMessage = Strings.nullToEmpty(commitMessage).trim();
checkState(!wellFormedMessage.isEmpty(), "Commit message cannot be null or empty");
wellFormedMessage = wellFormedMessage + "\n";
@@ -372,16 +449,21 @@
private static RevCommit lookupCommit(Repository repository, PatchSet patchSet)
throws IOException {
ObjectId patchSetCommitId = getPatchSetCommitId(patchSet);
+ return lookupCommit(repository, patchSetCommitId);
+ }
+
+ private static RevCommit lookupCommit(Repository repository, ObjectId commitId)
+ throws IOException {
try (RevWalk revWalk = new RevWalk(repository)) {
- return revWalk.parseCommit(patchSetCommitId);
+ return revWalk.parseCommit(commitId);
}
}
private static ObjectId createNewTree(
- Repository repository, RevCommit baseCommit, TreeModification treeModification)
+ Repository repository, RevCommit baseCommit, List<TreeModification> treeModifications)
throws IOException, InvalidChangeOperationException {
TreeCreator treeCreator = new TreeCreator(baseCommit);
- treeCreator.addTreeModification(treeModification);
+ treeCreator.addTreeModifications(treeModifications);
ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository);
if (ObjectId.equals(newTreeId, baseCommit.getTree())) {
@@ -390,7 +472,7 @@
return newTreeId;
}
- private ObjectId merge(Repository repository, ChangeEdit changeEdit, ObjectId newTreeId)
+ private static ObjectId merge(Repository repository, ChangeEdit changeEdit, ObjectId newTreeId)
throws IOException, MergeConflictException {
PatchSet basePatchSet = changeEdit.getBasePatchSet();
ObjectId basePatchSetCommitId = getPatchSetCommitId(basePatchSet);
@@ -436,17 +518,20 @@
return ObjectId.fromString(patchSet.getRevision().get());
}
- private void createEditReference(
+ private ChangeEdit createEdit(
Repository repository,
ChangeControl changeControl,
PatchSet basePatchSet,
- ObjectId newEditCommit,
+ ObjectId newEditCommitId,
Timestamp timestamp)
throws IOException, OrmException {
Change change = changeControl.getChange();
String editRefName = getEditRefName(change, basePatchSet);
- updateReference(repository, editRefName, ObjectId.zeroId(), newEditCommit, timestamp);
+ updateReference(repository, editRefName, ObjectId.zeroId(), newEditCommitId, timestamp);
reindex(change);
+
+ RevCommit newEditCommit = lookupCommit(repository, newEditCommitId);
+ return new ChangeEdit(change, editRefName, newEditCommit, basePatchSet);
}
private String getEditRefName(Change change, PatchSet basePatchSet) {
@@ -454,13 +539,17 @@
return RefNames.refsEdit(me.getAccountId(), change.getId(), basePatchSet.getId());
}
- private void updateEditReference(
- Repository repository, ChangeEdit changeEdit, ObjectId newEditCommit, Timestamp timestamp)
+ private ChangeEdit updateEdit(
+ Repository repository, ChangeEdit changeEdit, ObjectId newEditCommitId, Timestamp timestamp)
throws IOException, OrmException {
String editRefName = changeEdit.getRefName();
RevCommit currentEditCommit = changeEdit.getEditCommit();
- updateReference(repository, editRefName, currentEditCommit, newEditCommit, timestamp);
+ updateReference(repository, editRefName, currentEditCommit, newEditCommitId, timestamp);
reindex(changeEdit.getChange());
+
+ RevCommit newEditCommit = lookupCommit(repository, newEditCommitId);
+ return new ChangeEdit(
+ changeEdit.getChange(), editRefName, newEditCommit, changeEdit.getBasePatchSet());
}
private void updateReference(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 0cfdeed..60abd38 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -149,7 +149,7 @@
try (RevWalk rw = new RevWalk(repo)) {
RevCommit commit = rw.parseCommit(ref.getObjectId());
PatchSet basePs = getBasePatchSet(ctl, ref);
- return Optional.of(new ChangeEdit(u, change, ref, commit, basePs));
+ return Optional.of(new ChangeEdit(change, ref.getName(), commit, basePs));
}
}
}
@@ -157,6 +157,7 @@
/**
* Promote change edit to patch set, by squashing the edit into its parent.
*
+ * @param ctl the {@code ChangeControl} of the change to which the change edit belongs
* @param edit change edit to publish
* @param notify Notify handling that defines to whom email notifications should be sent after the
* change edit is published.
@@ -167,6 +168,7 @@
* @throws RestApiException
*/
public void publish(
+ ChangeControl ctl,
final ChangeEdit edit,
NotifyHandling notify,
ListMultimap<RecipientType, Account.Id> accountsToNotify)
@@ -181,7 +183,6 @@
}
RevCommit squashed = squashEdit(rw, oi, edit.getEditCommit(), basePatchSet);
- ChangeControl ctl = changeControlFactory.controlFor(db.get(), change, edit.getUser());
PatchSet.Id psId = ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId());
PatchSetInserter inserter =
patchSetInserterFactory
@@ -194,7 +195,8 @@
// Previously checked that the base patch set is the current patch set.
ObjectId prior = ObjectId.fromString(basePatchSet.getRevision().get());
- ChangeKind kind = changeKindCache.getChangeKind(change.getProject(), repo, prior, squashed);
+ ChangeKind kind =
+ changeKindCache.getChangeKind(change.getProject(), repo, rw, prior, squashed);
if (kind == ChangeKind.NO_CODE_CHANGE) {
message.append("Commit message was updated.");
inserter.setDescription("Edit commit message");
@@ -280,7 +282,7 @@
private static void deleteRef(Repository repo, ChangeEdit edit) throws IOException {
String refName = edit.getRefName();
RefUpdate ru = repo.updateRef(refName, true);
- ru.setExpectedOldObjectId(edit.getRef().getObjectId());
+ ru.setExpectedOldObjectId(edit.getEditCommit());
ru.setForceUpdate(true);
RefUpdate.Result result = ru.delete();
switch (result) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
index dc35309..3d75e6a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteStreams;
import com.google.gerrit.extensions.restapi.RawInput;
import java.io.IOException;
@@ -53,6 +54,16 @@
return Collections.singletonList(changeContentEdit);
}
+ @Override
+ public String getFilePath() {
+ return filePath;
+ }
+
+ @VisibleForTesting
+ RawInput getNewContent() {
+ return newContent;
+ }
+
/** A {@code PathEdit} which changes the contents of a file. */
private static class ChangeContent extends DirCacheEditor.PathEdit {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java
index 62da19a..feffb70 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java
@@ -34,4 +34,9 @@
DirCacheEditor.DeletePath deletePathEdit = new DirCacheEditor.DeletePath(filePath);
return Collections.singletonList(deletePathEdit);
}
+
+ @Override
+ public String getFilePath() {
+ return filePath;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java
index aeacd23..b847599 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java
@@ -52,4 +52,9 @@
}
}
}
+
+ @Override
+ public String getFilePath() {
+ return newFilePath;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java
index 1bd55f6..393a866 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java
@@ -58,4 +58,9 @@
}
}
}
+
+ @Override
+ public String getFilePath() {
+ return filePath;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java
index 7e9a96a..e867e76 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java
@@ -36,8 +36,6 @@
public class TreeCreator {
private final RevCommit baseCommit;
- // At the moment, a list wouldn't be necessary as only one modification is
- // applied per created tree. This is going to change in the near future.
private final List<TreeModification> treeModifications = new ArrayList<>();
public TreeCreator(RevCommit baseCommit) {
@@ -45,14 +43,14 @@
}
/**
- * Apply a modification to the tree which is taken as a basis. If this method is called multiple
+ * Apply modifications to the tree which is taken as a basis. If this method is called multiple
* times, the modifications are applied subsequently in exactly the order they were provided.
*
- * @param treeModification a modification which should be applied to the base tree
+ * @param treeModifications modifications which should be applied to the base tree
*/
- public void addTreeModification(TreeModification treeModification) {
- checkNotNull(treeModification, "treeModification must not be null");
- treeModifications.add(treeModification);
+ public void addTreeModifications(List<TreeModification> treeModifications) {
+ checkNotNull(treeModifications, "treeModifications must not be null");
+ this.treeModifications.addAll(treeModifications);
}
/**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java
index 217a309..2656707 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.edit.tree;
+import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.util.List;
import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -35,4 +36,14 @@
*/
List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, RevCommit baseCommit)
throws IOException;
+
+ /**
+ * Indicates a file path which is affected by this {@code TreeModification}. If the modification
+ * refers to several file paths (e.g. renaming a file), returning either of them is appropriate as
+ * long as the returned value is deterministic.
+ *
+ * @return an affected file path
+ */
+ @VisibleForTesting
+ String getFilePath();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java
index 17fc52b..c0f9c29 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java
@@ -16,14 +16,19 @@
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.IdentifiedUser;
+import java.io.IOException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
-public class CommitReceivedEvent extends RefEvent {
+public class CommitReceivedEvent extends RefEvent implements AutoCloseable {
static final String TYPE = "commit-received";
public ReceiveCommand command;
public Project project;
public String refName;
+ public RevWalk revWalk;
public RevCommit commit;
public IdentifiedUser user;
@@ -35,14 +40,18 @@
ReceiveCommand command,
Project project,
String refName,
- RevCommit commit,
- IdentifiedUser user) {
+ ObjectReader reader,
+ ObjectId commitId,
+ IdentifiedUser user)
+ throws IOException {
this();
this.command = command;
this.project = project;
this.refName = refName;
- this.commit = commit;
+ this.revWalk = new RevWalk(reader);
+ this.commit = revWalk.parseCommit(commitId);
this.user = user;
+ revWalk.parseBody(commit);
}
@Override
@@ -54,4 +63,9 @@
public String getRefName() {
return refName;
}
+
+ @Override
+ public void close() {
+ revWalk.close();
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index 80dcb78..ce04f26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -311,7 +311,7 @@
// set whose parent matches this patch set's revision.
for (ChangeData cd :
queryProvider.get().byProjectGroups(change.getProject(), currentPs.getGroups())) {
- patchSets:
+ PATCH_SETS:
for (PatchSet ps : cd.patchSets()) {
RevCommit commit = rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
for (RevCommit p : commit.getParents()) {
@@ -319,7 +319,7 @@
continue;
}
ca.neededBy.add(newNeededBy(checkNotNull(cd.change()), ps));
- continue patchSets;
+ continue PATCH_SETS;
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java b/gerrit-server/src/main/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
new file mode 100644
index 0000000..3fc786b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
@@ -0,0 +1,154 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.fixes;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.common.RawInputUtil;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.FixReplacement;
+import com.google.gerrit.server.change.FileContentUtil;
+import com.google.gerrit.server.edit.tree.ChangeFileContentModification;
+import com.google.gerrit.server.edit.tree.TreeModification;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+/** An interpreter for {@code FixReplacement}s. */
+@Singleton
+public class FixReplacementInterpreter {
+
+ private static final Comparator<FixReplacement> ASC_RANGE_FIX_REPLACEMENT_COMPARATOR =
+ Comparator.comparing(fixReplacement -> fixReplacement.range);
+
+ private final FileContentUtil fileContentUtil;
+
+ @Inject
+ public FixReplacementInterpreter(FileContentUtil fileContentUtil) {
+ this.fileContentUtil = fileContentUtil;
+ }
+
+ /**
+ * Transforms the given {@code FixReplacement}s into {@code TreeModification}s.
+ *
+ * @param repository the affected Git repository
+ * @param projectState the affected project
+ * @param patchSetCommitId the patch set which should be modified
+ * @param fixReplacements the replacements which should be applied
+ * @return a list of {@code TreeModification}s representing the given replacements
+ * @throws ResourceNotFoundException if a file to which one of the replacements refers doesn't
+ * exist
+ * @throws ResourceConflictException if the replacements can't be transformed into {@code
+ * TreeModification}s
+ */
+ public List<TreeModification> toTreeModifications(
+ Repository repository,
+ ProjectState projectState,
+ ObjectId patchSetCommitId,
+ List<FixReplacement> fixReplacements)
+ throws ResourceNotFoundException, IOException, ResourceConflictException {
+ checkNotNull(fixReplacements, "Fix replacements must not be null");
+
+ Map<String, List<FixReplacement>> fixReplacementsPerFilePath =
+ fixReplacements
+ .stream()
+ .collect(Collectors.groupingBy(fixReplacement -> fixReplacement.path));
+
+ List<TreeModification> treeModifications = new ArrayList<>();
+ for (Map.Entry<String, List<FixReplacement>> entry : fixReplacementsPerFilePath.entrySet()) {
+ TreeModification treeModification =
+ toTreeModification(
+ repository, projectState, patchSetCommitId, entry.getKey(), entry.getValue());
+ treeModifications.add(treeModification);
+ }
+ return treeModifications;
+ }
+
+ private TreeModification toTreeModification(
+ Repository repository,
+ ProjectState projectState,
+ ObjectId patchSetCommitId,
+ String filePath,
+ List<FixReplacement> fixReplacements)
+ throws ResourceNotFoundException, IOException, ResourceConflictException {
+ String fileContent = getFileContent(repository, projectState, patchSetCommitId, filePath);
+ String newFileContent = getNewFileContent(fileContent, fixReplacements);
+ return new ChangeFileContentModification(filePath, RawInputUtil.create(newFileContent));
+ }
+
+ private String getFileContent(
+ Repository repository, ProjectState projectState, ObjectId patchSetCommitId, String filePath)
+ throws ResourceNotFoundException, IOException {
+ try (BinaryResult fileContent =
+ fileContentUtil.getContent(repository, projectState, patchSetCommitId, filePath)) {
+ return fileContent.asString();
+ }
+ }
+
+ private static String getNewFileContent(String fileContent, List<FixReplacement> fixReplacements)
+ throws ResourceConflictException {
+ List<FixReplacement> sortedReplacements = new ArrayList<>(fixReplacements);
+ sortedReplacements.sort(ASC_RANGE_FIX_REPLACEMENT_COMPARATOR);
+
+ LineIdentifier lineIdentifier = new LineIdentifier(fileContent);
+ StringModifier fileContentModifier = new StringModifier(fileContent);
+ for (FixReplacement fixReplacement : sortedReplacements) {
+ Comment.Range range = fixReplacement.range;
+ try {
+ int startLineIndex = lineIdentifier.getStartIndexOfLine(range.startLine);
+ int startLineLength = lineIdentifier.getLengthOfLine(range.startLine);
+
+ int endLineIndex = lineIdentifier.getStartIndexOfLine(range.endLine);
+ int endLineLength = lineIdentifier.getLengthOfLine(range.endLine);
+
+ if (range.startChar > startLineLength || range.endChar > endLineLength) {
+ throw new ResourceConflictException(
+ String.format(
+ "Range %s refers to a non-existent offset (start line length: %s,"
+ + " end line length: %s)",
+ toString(range), startLineLength, endLineLength));
+ }
+
+ int startIndex = startLineIndex + range.startChar;
+ int endIndex = endLineIndex + range.endChar;
+ fileContentModifier.replace(startIndex, endIndex, fixReplacement.replacement);
+ } catch (StringIndexOutOfBoundsException e) {
+ // Most of the StringIndexOutOfBoundsException should never occur because we reject fix
+ // replacements for invalid ranges. However, we can't cover all cases for efficiency
+ // reasons. For instance, we don't determine the number of lines in a file. That's why we
+ // need to map this exception and thus provide a meaningful error.
+ throw new ResourceConflictException(
+ String.format("Cannot apply fix replacement for range %s", toString(range)), e);
+ }
+ }
+ return fileContentModifier.getResult();
+ }
+
+ private static String toString(Comment.Range range) {
+ return String.format(
+ "(%s:%s - %s:%s)", range.startLine, range.startChar, range.endLine, range.endChar);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/fixes/LineIdentifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/fixes/LineIdentifier.java
new file mode 100644
index 0000000..c32d822
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/fixes/LineIdentifier.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.fixes;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An identifier of lines in a string. Lines are sequences of characters which are separated by any
+ * Unicode linebreak sequence as defined by the regular expression {@code \R}. If data for several
+ * lines is requested, calls which are ordered according to ascending line numbers are the most
+ * efficient.
+ */
+class LineIdentifier {
+
+ private static final Pattern LINE_SEPARATOR_PATTERN = Pattern.compile("\\R");
+ private final Matcher lineSeparatorMatcher;
+
+ private int nextLineNumber;
+ private int nextLineStartIndex;
+ private int currentLineStartIndex;
+ private int currentLineEndIndex;
+
+ LineIdentifier(String string) {
+ checkNotNull(string);
+ lineSeparatorMatcher = LINE_SEPARATOR_PATTERN.matcher(string);
+ reset();
+ }
+
+ /**
+ * Returns the start index of the indicated line within the given string. Start indices are
+ * zero-based while line numbers are one-based.
+ *
+ * <p><b>Note:</b> Requesting data for several lines is more efficient if those calls occur with
+ * increasing line number.
+ *
+ * @param lineNumber the line whose start index should be determined
+ * @return the start index of the line
+ * @throws StringIndexOutOfBoundsException if the line number is negative, zero or greater than
+ * the identified number of lines
+ */
+ public int getStartIndexOfLine(int lineNumber) {
+ findLine(lineNumber);
+ return currentLineStartIndex;
+ }
+
+ /**
+ * Returns the length of the indicated line in the given string. The character(s) used to separate
+ * lines aren't included in the count. Line numbers are one-based.
+ *
+ * <p><b>Note:</b> Requesting data for several lines is more efficient if those calls occur with
+ * increasing line number.
+ *
+ * @param lineNumber the line whose length should be determined
+ * @return the length of the line
+ * @throws StringIndexOutOfBoundsException if the line number is negative, zero or greater than
+ * the identified number of lines
+ */
+ public int getLengthOfLine(int lineNumber) {
+ findLine(lineNumber);
+ return currentLineEndIndex - currentLineStartIndex;
+ }
+
+ private void findLine(int targetLineNumber) {
+ if (targetLineNumber <= 0) {
+ throw new StringIndexOutOfBoundsException("Line number must be positive");
+ }
+ if (targetLineNumber < nextLineNumber) {
+ reset();
+ }
+ while (nextLineNumber < targetLineNumber + 1 && lineSeparatorMatcher.find()) {
+ currentLineStartIndex = nextLineStartIndex;
+ currentLineEndIndex = lineSeparatorMatcher.start();
+ nextLineStartIndex = lineSeparatorMatcher.end();
+ nextLineNumber++;
+ }
+
+ // End of string
+ if (nextLineNumber == targetLineNumber) {
+ currentLineStartIndex = nextLineStartIndex;
+ currentLineEndIndex = lineSeparatorMatcher.regionEnd();
+ }
+ if (nextLineNumber < targetLineNumber) {
+ throw new StringIndexOutOfBoundsException(
+ String.format("Line %d isn't available", targetLineNumber));
+ }
+ }
+
+ private void reset() {
+ nextLineNumber = 1;
+ nextLineStartIndex = 0;
+ currentLineStartIndex = 0;
+ currentLineEndIndex = 0;
+ lineSeparatorMatcher.reset();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/fixes/StringModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/fixes/StringModifier.java
new file mode 100644
index 0000000..ccd40b3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/fixes/StringModifier.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.fixes;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A modifier of a string. It allows to replace multiple parts of a string by indicating those parts
+ * with indices based on the unmodified string. There is one limitation though: Replacements which
+ * affect lower indices of the string must be specified before replacements for higher indices.
+ */
+class StringModifier {
+
+ private final StringBuilder stringBuilder;
+
+ private int characterShift = 0;
+ private int previousEndOffset = Integer.MIN_VALUE;
+
+ StringModifier(String string) {
+ checkNotNull(string, "string must not be null");
+ stringBuilder = new StringBuilder(string);
+ }
+
+ /**
+ * Replaces part of the string with another content. When called multiple times, the calls must be
+ * ordered according to increasing start indices. Overlapping replacement regions aren't
+ * supported.
+ *
+ * @param startIndex the beginning index in the unmodified string (inclusive)
+ * @param endIndex the ending index in the unmodified string (exclusive)
+ * @param replacement the string which should be used instead of the original content
+ * @throws StringIndexOutOfBoundsException if the start index is smaller than the end index of a
+ * previous call of this method
+ */
+ public void replace(int startIndex, int endIndex, String replacement) {
+ checkNotNull(replacement, "replacement string must not be null");
+ if (previousEndOffset > startIndex) {
+ throw new StringIndexOutOfBoundsException(
+ String.format(
+ "Not supported to replace the content starting at index %s after previous "
+ + "replacement which ended at index %s",
+ startIndex, previousEndOffset));
+ }
+ int shiftedStartIndex = startIndex + characterShift;
+ int shiftedEndIndex = endIndex + characterShift;
+ if (shiftedEndIndex > stringBuilder.length()) {
+ throw new StringIndexOutOfBoundsException(
+ String.format("end %s > length %s", shiftedEndIndex, stringBuilder.length()));
+ }
+ stringBuilder.replace(shiftedStartIndex, shiftedEndIndex, replacement);
+
+ int replacedContentLength = endIndex - startIndex;
+ characterShift += replacement.length() - replacedContentLength;
+ previousEndOffset = endIndex;
+ }
+
+ /**
+ * Returns the modified string including all specified replacements.
+ *
+ * @return the modified string
+ */
+ public String getResult() {
+ return stringBuilder.toString();
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/AbandonOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AbandonOp.java
index 99b647a..c5376fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/AbandonOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/AbandonOp.java
@@ -34,8 +34,8 @@
import com.google.gerrit.server.update.ChangeContext;
import com.google.gerrit.server.update.Context;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -64,7 +64,7 @@
@Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify);
}
- @AssistedInject
+ @Inject
AbandonOp(
AbandonedSender.Factory abandonedSenderFactory,
ChangeMessagesUtil cmUtil,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
index e680ea7..46916c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
@@ -23,8 +23,8 @@
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.util.RequestId;
import com.google.gerrit.server.util.SubmoduleSectionParser;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
@@ -55,7 +55,7 @@
private final RequestId submissionId;
Set<SubmoduleSubscription> subscriptions;
- @AssistedInject
+ @Inject
GitModules(
@CanonicalWebUrl @Nullable String canonicalWebUrl,
@Assisted Branch.NameKey branch,
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 1511da0..b153943 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
@@ -521,7 +521,6 @@
db,
or.repo,
or.rw,
- or.ins,
or.canMergeFlag,
getAlreadyAccepted(or, ob.oldTip),
allCommits,
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 a7c8b53..9439a8b 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
@@ -34,9 +34,9 @@
import com.google.gerrit.server.update.Context;
import com.google.gerrit.server.util.RequestScopePropagator;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.ExecutorService;
@@ -74,7 +74,7 @@
private PatchSet patchSet;
private PatchSetInfo info;
- @AssistedInject
+ @Inject
MergedByPushOp(
PatchSetInfoFactory patchSetInfoFactory,
ChangeMessagesUtil cmUtil,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
index e3b1ad6..b057c92 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -22,7 +22,6 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.BatchRefUpdate;
@@ -185,7 +184,7 @@
private boolean closeRepository;
private IdentifiedUser author;
- @AssistedInject
+ @Inject
public MetaDataUpdate(
GitReferenceUpdated gitRefUpdated,
@Assisted Project.NameKey projectName,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
index 20f053a..ac031eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
@@ -64,18 +64,15 @@
}
public <T> Callable<T> scope(RequestContext requestContext, Callable<T> callable) {
- final Context ctx = new Context();
- final Callable<T> wrapped = context(requestContext, cleanup(callable));
- return new Callable<T>() {
- @Override
- public T call() throws Exception {
- Context old = current.get();
- current.set(ctx);
- try {
- return wrapped.call();
- } finally {
- current.set(old);
- }
+ Context ctx = new Context();
+ Callable<T> wrapped = context(requestContext, cleanup(callable));
+ return () -> {
+ Context old = current.get();
+ current.set(ctx);
+ try {
+ return wrapped.call();
+ } finally {
+ current.set(old);
}
};
}
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 61d8cfe..f1a35d7 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
@@ -155,6 +155,9 @@
ImmutableSet.of(
"MaxWithBlock", "AnyWithBlock", "MaxNoBlock", "NoBlock", "NoOp", "PatchSetLock");
+ private static final String REVIEWER = "reviewer";
+ private static final String KEY_ENABLE_REVIEWER_BY_EMAIL = "enableByEmail";
+
private static final String LEGACY_PERMISSION_PUSH_TAG = "pushTag";
private static final String LEGACY_PERMISSION_PUSH_SIGNED_TAG = "pushSignedTag";
@@ -163,6 +166,9 @@
private static final SubmitType DEFAULT_SUBMIT_ACTION = SubmitType.MERGE_IF_NECESSARY;
private static final ProjectState DEFAULT_STATE_VALUE = ProjectState.ACTIVE;
+ private static final String EXTENSION_PANELS = "extension-panels";
+ private static final String KEY_PANEL = "panel";
+
private Project.NameKey projectName;
private Project project;
private AccountsSection accountsSection;
@@ -182,6 +188,7 @@
private boolean checkReceivedObjects;
private Set<String> sectionsWithUnknownPermissions;
private boolean hasLegacyPermissions;
+ private Map<String, List<String>> extensionPanelSections;
public static ProjectConfig read(MetaDataUpdate update)
throws IOException, ConfigInvalidException {
@@ -197,6 +204,10 @@
return r;
}
+ public Map<String, List<String>> getExtensionPanelSections() {
+ return extensionPanelSections;
+ }
+
public static CommentLinkInfoImpl buildCommentLink(Config cfg, String name, boolean allowRaw)
throws IllegalArgumentException {
String match = cfg.getString(COMMENTLINK, name, KEY_MATCH);
@@ -507,6 +518,8 @@
p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
p.setRejectImplicitMerges(
getEnum(rc, RECEIVE, null, KEY_REJECT_IMPLICIT_MERGES, InheritableBoolean.INHERIT));
+ p.setEnableReviewerByEmail(
+ getEnum(rc, REVIEWER, null, KEY_ENABLE_REVIEWER_BY_EMAIL, InheritableBoolean.INHERIT));
p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, DEFAULT_SUBMIT_ACTION));
p.setUseContentMerge(getEnum(rc, SUBMIT, null, KEY_MERGE_CONTENT, InheritableBoolean.INHERIT));
@@ -526,6 +539,7 @@
mimeTypes = new ConfiguredMimeTypes(projectName.get(), rc);
loadPluginSections(rc);
loadReceiveSection(rc);
+ loadExtensionPanelSections(rc);
}
private void loadAccountsSection(Config rc, Map<String, GroupReference> groupsByName) {
@@ -534,6 +548,25 @@
loadPermissionRules(rc, ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY, groupsByName, false));
}
+ private void loadExtensionPanelSections(Config rc) {
+ Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
+ extensionPanelSections = Maps.newLinkedHashMap();
+ for (String name : rc.getSubsections(EXTENSION_PANELS)) {
+ String lower = name.toLowerCase();
+ if (lowerNames.containsKey(lower)) {
+ error(
+ new ValidationError(
+ PROJECT_CONFIG,
+ String.format(
+ "Extension Panels \"%s\" conflicts with \"%s\"", name, lowerNames.get(lower))));
+ }
+ lowerNames.put(lower, name);
+ extensionPanelSections.put(
+ name,
+ new ArrayList<>(Arrays.asList(rc.getStringList(EXTENSION_PANELS, name, KEY_PANEL))));
+ }
+ }
+
private void loadContributorAgreements(Config rc, Map<String, GroupReference> groupsByName) {
contributorAgreements = new HashMap<>();
for (String name : rc.getSubsections(CONTRIBUTOR_AGREEMENT)) {
@@ -1052,6 +1085,13 @@
KEY_REJECT_IMPLICIT_MERGES,
p.getRejectImplicitMerges(),
InheritableBoolean.INHERIT);
+ set(
+ rc,
+ REVIEWER,
+ null,
+ KEY_ENABLE_REVIEWER_BY_EMAIL,
+ p.getEnableReviewerByEmail(),
+ InheritableBoolean.INHERIT);
set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), DEFAULT_SUBMIT_ACTION);
set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), InheritableBoolean.INHERIT);
@@ -1288,40 +1328,55 @@
setBooleanConfigKey(
rc,
+ LABEL,
name,
KEY_ALLOW_POST_SUBMIT,
label.allowPostSubmit(),
LabelType.DEF_ALLOW_POST_SUBMIT);
setBooleanConfigKey(
- rc, name, KEY_COPY_MIN_SCORE, label.isCopyMinScore(), LabelType.DEF_COPY_MIN_SCORE);
- setBooleanConfigKey(
- rc, name, KEY_COPY_MAX_SCORE, label.isCopyMaxScore(), LabelType.DEF_COPY_MAX_SCORE);
+ rc,
+ LABEL,
+ name,
+ KEY_COPY_MIN_SCORE,
+ label.isCopyMinScore(),
+ LabelType.DEF_COPY_MIN_SCORE);
setBooleanConfigKey(
rc,
+ LABEL,
+ name,
+ KEY_COPY_MAX_SCORE,
+ label.isCopyMaxScore(),
+ LabelType.DEF_COPY_MAX_SCORE);
+ setBooleanConfigKey(
+ rc,
+ LABEL,
name,
KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE,
label.isCopyAllScoresOnTrivialRebase(),
LabelType.DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE);
setBooleanConfigKey(
rc,
+ LABEL,
name,
KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE,
label.isCopyAllScoresIfNoCodeChange(),
LabelType.DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE);
setBooleanConfigKey(
rc,
+ LABEL,
name,
KEY_COPY_ALL_SCORES_IF_NO_CHANGE,
label.isCopyAllScoresIfNoChange(),
LabelType.DEF_COPY_ALL_SCORES_IF_NO_CHANGE);
setBooleanConfigKey(
rc,
+ LABEL,
name,
KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE,
label.isCopyAllScoresOnMergeFirstParentUpdate(),
LabelType.DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE);
setBooleanConfigKey(
- rc, name, KEY_CAN_OVERRIDE, label.canOverride(), LabelType.DEF_CAN_OVERRIDE);
+ rc, LABEL, name, KEY_CAN_OVERRIDE, label.canOverride(), LabelType.DEF_CAN_OVERRIDE);
List<String> values = Lists.newArrayListWithCapacity(label.getValues().size());
for (LabelValue value : label.getValues()) {
values.add(value.format());
@@ -1335,11 +1390,11 @@
}
private static void setBooleanConfigKey(
- Config rc, String name, String key, boolean value, boolean defaultValue) {
+ Config rc, String section, String name, String key, boolean value, boolean defaultValue) {
if (value == defaultValue) {
- rc.unset(LABEL, name, key);
+ rc.unset(section, name, key);
} else {
- rc.setBoolean(LABEL, name, key, value);
+ rc.setBoolean(section, name, key, value);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 399dfc7..189df45 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -1272,6 +1272,12 @@
@Option(name = "--draft", usage = "mark new/updated changes as draft")
boolean draft;
+ @Option(name = "--private", usage = "mark new/updated change as private")
+ boolean isPrivate;
+
+ @Option(name = "--remove-private", usage = "remove privacy flag from updated change")
+ boolean removePrivate;
+
@Option(
name = "--edit",
aliases = {"-e"},
@@ -1525,6 +1531,11 @@
return;
}
+ if (magicBranch.isPrivate && magicBranch.removePrivate) {
+ reject(cmd, "the options 'private' and 'remove-private' are mutually exclusive");
+ return;
+ }
+
if (magicBranch.draft && magicBranch.submit) {
reject(cmd, "cannot submit draft");
return;
@@ -2134,6 +2145,7 @@
changeInserterFactory
.create(changeId, commit, refName)
.setTopic(magicBranch.topic)
+ .setPrivate(magicBranch.isPrivate)
// Changes already validated in validateNewCommits.
.setValidatePolicy(CommitValidators.Policy.NONE);
@@ -2185,6 +2197,7 @@
.setUpdateRef(false)
.setPatchSetDescription(magicBranch.message));
if (!magicBranch.hashtags.isEmpty()) {
+ // Any change owner is allowed to add hashtags when creating a change.
bu.addOp(
changeId,
hashtagsFactory.create(new HashtagsInput(magicBranch.hashtags)).setFireEvent(false));
@@ -2468,13 +2481,12 @@
if (edit.get().getBasePatchSet().getId().equals(psId)) {
// replace edit
cmd =
- new ReceiveCommand(
- edit.get().getRef().getObjectId(), newCommitId, edit.get().getRefName());
+ new ReceiveCommand(edit.get().getEditCommit(), newCommitId, edit.get().getRefName());
} else {
// delete old edit ref on rebase
prev =
new ReceiveCommand(
- edit.get().getRef().getObjectId(), ObjectId.zeroId(), edit.get().getRefName());
+ edit.get().getEditCommit(), ObjectId.zeroId(), edit.get().getRefName());
createEditCommand();
}
} else {
@@ -2751,8 +2763,6 @@
RevCommit c = rw.parseCommit(id);
rw.parseBody(c);
- CommitReceivedEvent receiveEvent =
- new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, user);
CommitValidators.Policy policy;
if (magicBranch != null
@@ -2763,7 +2773,8 @@
policy = CommitValidators.Policy.RECEIVE_COMMITS;
}
- try {
+ try (CommitReceivedEvent receiveEvent =
+ new CommitReceivedEvent(cmd, project, ctl.getRefName(), rw.getObjectReader(), c, user)) {
messages.addAll(
commitValidatorsFactory.create(policy, ctl, sshInfo, repo).validate(receiveEvent));
} catch (CommitValidationException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
index 063f395..723fb6f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
@@ -17,6 +17,7 @@
import static com.google.gerrit.common.data.GlobalCapability.BATCH_CHANGES_LIMIT;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -39,8 +40,9 @@
}
public int getEffectiveMaxBatchChangesLimit(CurrentUser user) {
- if (user.getCapabilities().canPerform(BATCH_CHANGES_LIMIT)) {
- return user.getCapabilities().getRange(BATCH_CHANGES_LIMIT).getMax();
+ CapabilityControl cap = user.getCapabilities();
+ if (cap.hasExplicitRange(BATCH_CHANGES_LIMIT)) {
+ return cap.getRange(BATCH_CHANGES_LIMIT).getMax();
}
return systemMaxBatchChanges;
}
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 6ac5da1..b2c4ae9 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
@@ -55,8 +55,8 @@
import com.google.gerrit.server.update.RepoContext;
import com.google.gerrit.server.util.RequestScopePropagator;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import com.google.inject.util.Providers;
import java.io.IOException;
import java.util.HashMap;
@@ -83,9 +83,9 @@
Branch.NameKey dest,
boolean checkMergedInto,
@Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
- @Assisted("priorCommit") RevCommit priorCommit,
+ @Assisted("priorCommitId") ObjectId priorCommit,
@Assisted("patchSetId") PatchSet.Id patchSetId,
- @Assisted("commit") RevCommit commit,
+ @Assisted("commitId") ObjectId commitId,
PatchSetInfo info,
List<String> groups,
@Nullable MagicBranchInput magicBranch,
@@ -115,9 +115,9 @@
private final Branch.NameKey dest;
private final boolean checkMergedInto;
private final PatchSet.Id priorPatchSetId;
- private final RevCommit priorCommit;
+ private final ObjectId priorCommitId;
private final PatchSet.Id patchSetId;
- private final RevCommit commit;
+ private final ObjectId commitId;
private final PatchSetInfo info;
private final MagicBranchInput magicBranch;
private final PushCertificate pushCertificate;
@@ -125,6 +125,7 @@
private final Map<String, Short> approvals = new HashMap<>();
private final MailRecipients recipients = new MailRecipients();
+ private RevCommit commit;
private Change change;
private PatchSet newPatchSet;
private ChangeKind changeKind;
@@ -134,7 +135,7 @@
private RequestScopePropagator requestScopePropagator;
private boolean updateRef;
- @AssistedInject
+ @Inject
ReplaceOp(
AccountResolver accountResolver,
ApprovalCopier approvalCopier,
@@ -154,9 +155,9 @@
@Assisted Branch.NameKey dest,
@Assisted boolean checkMergedInto,
@Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId,
- @Assisted("priorCommit") RevCommit priorCommit,
+ @Assisted("priorCommitId") ObjectId priorCommitId,
@Assisted("patchSetId") PatchSet.Id patchSetId,
- @Assisted("commit") RevCommit commit,
+ @Assisted("commitId") ObjectId commitId,
@Assisted PatchSetInfo info,
@Assisted List<String> groups,
@Assisted @Nullable MagicBranchInput magicBranch,
@@ -180,9 +181,9 @@
this.dest = dest;
this.checkMergedInto = checkMergedInto;
this.priorPatchSetId = priorPatchSetId;
- this.priorCommit = priorCommit;
+ this.priorCommitId = priorCommitId.copy();
this.patchSetId = patchSetId;
- this.commit = commit;
+ this.commitId = commitId.copy();
this.info = info;
this.groups = groups;
this.magicBranch = magicBranch;
@@ -192,9 +193,15 @@
@Override
public void updateRepo(RepoContext ctx) throws Exception {
+ commit = ctx.getRevWalk().parseCommit(commitId);
+ ctx.getRevWalk().parseBody(commit);
changeKind =
changeKindCache.getChangeKind(
- projectControl.getProject().getNameKey(), ctx.getRepository(), priorCommit, commit);
+ projectControl.getProject().getNameKey(),
+ ctx.getRepository(),
+ ctx.getRevWalk(),
+ priorCommitId,
+ commitId);
if (checkMergedInto) {
Ref mergedInto = findMergedInto(ctx, dest.get(), commit);
@@ -205,7 +212,7 @@
}
if (updateRef) {
- ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), commit, patchSetId.toRefName()));
+ ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), commitId, patchSetId.toRefName()));
}
}
@@ -240,6 +247,13 @@
if (magicBranch.topic != null && !magicBranch.topic.equals(ctx.getChange().getTopic())) {
update.setTopic(magicBranch.topic);
}
+ if (magicBranch.removePrivate) {
+ change.setPrivate(false);
+ update.setPrivate(false);
+ } else if (magicBranch.isPrivate) {
+ change.setPrivate(true);
+ update.setPrivate(true);
+ }
}
boolean draft = magicBranch != null && magicBranch.draft;
@@ -252,7 +266,7 @@
ctx.getRevWalk(),
update,
patchSetId,
- commit,
+ commitId,
draft,
groups,
pushCertificate != null ? pushCertificate.toTextWithSignature() : null,
@@ -376,7 +390,7 @@
List<String> idList = commit.getFooterLines(CHANGE_ID);
if (idList.isEmpty()) {
- change.setKey(new Change.Key("I" + commit.name()));
+ change.setKey(new Change.Key("I" + commitId.name()));
} else {
change.setKey(new Change.Key(idList.get(idList.size() - 1).trim()));
}
@@ -391,7 +405,7 @@
final Account account = ctx.getAccount();
if (!updateRef) {
gitRefUpdated.fire(
- ctx.getProject(), newPatchSet.getRefName(), ObjectId.zeroId(), commit, account);
+ ctx.getProject(), newPatchSet.getRefName(), ObjectId.zeroId(), commitId, account);
}
if (changeKind != ChangeKind.TRIVIAL_REBASE) {
@@ -498,7 +512,7 @@
return this;
}
- private Ref findMergedInto(Context ctx, String first, RevCommit commit) {
+ private static Ref findMergedInto(Context ctx, String first, RevCommit commit) {
try {
RefDatabase refDatabase = ctx.getRepository().getRefDatabase();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 56c0c44..7c236e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -35,8 +35,8 @@
import com.google.gerrit.server.update.RepoContext;
import com.google.gerrit.server.update.RepoOnlyOp;
import com.google.gerrit.server.update.UpdateException;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -117,7 +117,7 @@
// map of superproject and its branches which has submodule subscriptions
private final SetMultimap<Project.NameKey, Branch.NameKey> branchesByProject;
- @AssistedInject
+ @Inject
public SubmoduleOp(
GitModules.Factory gitmodulesFactory,
@GerritPersonIdent PersonIdent myIdent,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
index 2a6680c..e4c16bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
@@ -36,9 +36,6 @@
+ " onto a null tip; expected at least one fast-forward prior to"
+ " this operation");
}
- // TODO(dborowitz): args.rw is needed because it's a CodeReviewRevWalk.
- // When hoisting BatchUpdate into MergeOp, we will need to teach
- // BatchUpdate how to produce CodeReviewRevWalks.
CodeReviewCommit merged =
args.mergeUtil.mergeOneCommit(
caller,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
index 43ab01b..14fb138 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
@@ -116,9 +116,6 @@
public void updateRepoImpl(RepoContext ctx)
throws IntegrationException, InvalidChangeOperationException, RestApiException, IOException,
OrmException {
- // TODO(dborowitz): args.rw is needed because it's a CodeReviewRevWalk.
- // When hoisting BatchUpdate into MergeOp, we will need to teach
- // BatchUpdate how to produce CodeReviewRevWalks.
if (args.mergeUtil.canFastForward(
args.mergeSorter, args.mergeTip.getCurrentTip(), args.rw, toMerge)) {
if (!rebaseAlways) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index f721978..1c37c87 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -53,9 +53,9 @@
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.util.RequestId;
+import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -92,7 +92,6 @@
CodeReviewRevWalk rw,
IdentifiedUser caller,
MergeTip mergeTip,
- ObjectInserter inserter,
Repository repo,
RevFlag canMergeFlag,
ReviewDb db,
@@ -145,7 +144,7 @@
final MergeUtil mergeUtil;
final boolean dryrun;
- @AssistedInject
+ @Inject
Arguments(
AccountCache accountCache,
ApprovalsUtil approvalsUtil,
@@ -170,7 +169,6 @@
@Assisted CodeReviewRevWalk rw,
@Assisted IdentifiedUser caller,
@Assisted MergeTip mergeTip,
- @Assisted ObjectInserter inserter,
@Assisted Repository repo,
@Assisted RevFlag canMergeFlag,
@Assisted ReviewDb db,
@@ -204,7 +202,7 @@
this.rw = rw;
this.caller = caller;
this.mergeTip = mergeTip;
- this.inserter = inserter;
+ this.inserter = checkNotNull(rw.getObjectReader().getCreatedFromInserter());
this.repo = repo;
this.canMergeFlag = canMergeFlag;
this.db = db;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
index fc4817d..14c8a1c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -32,7 +32,6 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Set;
-import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
@@ -56,7 +55,6 @@
ReviewDb db,
Repository repo,
CodeReviewRevWalk rw,
- ObjectInserter inserter,
RevFlag canMergeFlag,
Set<RevCommit> alreadyAccepted,
Set<CodeReviewCommit> incoming,
@@ -78,7 +76,6 @@
rw,
caller,
mergeTip,
- inserter,
repo,
canMergeFlag,
db,
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 89bd560..e2feb80 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
@@ -105,6 +105,12 @@
@Override
public final void updateRepo(RepoContext ctx) throws Exception {
logDebug("{}#updateRepo for change {}", getClass().getSimpleName(), toMerge.change().getId());
+ checkState(
+ ctx.getRevWalk() == args.rw,
+ "SubmitStrategyOp requires callers to call BatchUpdate#setRepository with exactly the same"
+ + " CodeReviewRevWalk instance from the SubmitStrategy.Arguments: %s != %s",
+ ctx.getRevWalk(),
+ args.rw);
// Run the submit strategy implementation and record the merge tip state so
// we can create the ref update.
CodeReviewCommit tipBefore = args.mergeTip.getCurrentTip();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java
index 24ff379..07f3b21 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java
@@ -14,30 +14,30 @@
package com.google.gerrit.server.git.validators;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.server.validators.ValidationException;
-import java.util.Collections;
import java.util.List;
public class CommitValidationException extends ValidationException {
private static final long serialVersionUID = 1L;
- private final List<CommitValidationMessage> messages;
+ private final ImmutableList<CommitValidationMessage> messages;
public CommitValidationException(String reason, List<CommitValidationMessage> messages) {
super(reason);
- this.messages = messages;
+ this.messages = ImmutableList.copyOf(messages);
}
public CommitValidationException(String reason) {
super(reason);
- this.messages = Collections.emptyList();
+ this.messages = ImmutableList.of();
}
public CommitValidationException(String reason, Throwable why) {
super(reason, why);
- this.messages = Collections.emptyList();
+ this.messages = ImmutableList.of();
}
- public List<CommitValidationMessage> getMessages() {
+ public ImmutableList<CommitValidationMessage> getMessages() {
return messages;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
index 96aec3f..7590aea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
@@ -24,7 +24,7 @@
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables;
import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.SchemaUtil;
import java.sql.Timestamp;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
index 1b84e8e..36a409a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
@@ -33,7 +33,6 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -76,31 +75,28 @@
}
private SiteIndexer.Result reindexAccounts(
- final AccountIndex index, List<Account.Id> ids, ProgressMonitor progress) {
+ AccountIndex index, List<Account.Id> ids, ProgressMonitor progress) {
progress.beginTask("Reindexing accounts", ids.size());
List<ListenableFuture<?>> futures = new ArrayList<>(ids.size());
AtomicBoolean ok = new AtomicBoolean(true);
- final AtomicInteger done = new AtomicInteger();
- final AtomicInteger failed = new AtomicInteger();
+ AtomicInteger done = new AtomicInteger();
+ AtomicInteger failed = new AtomicInteger();
Stopwatch sw = Stopwatch.createStarted();
- for (final Account.Id id : ids) {
- final String desc = "account " + id;
+ for (Account.Id id : ids) {
+ String desc = "account " + id;
ListenableFuture<?> future =
executor.submit(
- new Callable<Void>() {
- @Override
- public Void call() throws Exception {
- try {
- accountCache.evict(id);
- index.replace(accountCache.get(id));
- verboseWriter.println("Reindexed " + desc);
- done.incrementAndGet();
- } catch (Exception e) {
- failed.incrementAndGet();
- throw e;
- }
- return null;
+ () -> {
+ try {
+ accountCache.evict(id);
+ index.replace(accountCache.get(id));
+ verboseWriter.println("Reindexed " + desc);
+ done.incrementAndGet();
+ } catch (Exception e) {
+ failed.incrementAndGet();
+ throw e;
}
+ return null;
});
addErrorListener(future, desc, progress, ok);
futures.add(future);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
index b8acadc..1f27d83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -40,12 +40,12 @@
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.Comment;
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.RefNames;
import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.index.FieldDef;
@@ -53,6 +53,7 @@
import com.google.gerrit.server.index.SchemaUtil;
import com.google.gerrit.server.index.change.StalenessChecker.RefState;
import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
@@ -73,8 +74,10 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
+import java.util.stream.Stream;
import org.eclipse.jgit.revwalk.FooterLine;
/**
@@ -184,6 +187,12 @@
public static final FieldDef<ChangeData, Iterable<String>> REVIEWER =
exact("reviewer2").stored().buildRepeatable(cd -> getReviewerFieldValues(cd.reviewers()));
+ /** Reviewer(s) associated with the change that do not have a gerrit account. */
+ public static final FieldDef<ChangeData, Iterable<String>> REVIEWER_BY_EMAIL =
+ exact("reviewer_by_email")
+ .stored()
+ .buildRepeatable(cd -> getReviewerByEmailFieldValues(cd.reviewersByEmail()));
+
@VisibleForTesting
static List<String> getReviewerFieldValues(ReviewerSet reviewers) {
List<String> r = new ArrayList<>(reviewers.asTable().size() * 2);
@@ -200,6 +209,27 @@
return state.toString() + ',' + id;
}
+ @VisibleForTesting
+ static List<String> getReviewerByEmailFieldValues(ReviewerByEmailSet reviewersByEmail) {
+ List<String> r = new ArrayList<>(reviewersByEmail.asTable().size() * 2);
+ for (Table.Cell<ReviewerStateInternal, Address, Timestamp> c :
+ reviewersByEmail.asTable().cellSet()) {
+ String v = getReviewerByEmailFieldValue(c.getRowKey(), c.getColumnKey());
+ r.add(v);
+ if (c.getColumnKey().getName() != null) {
+ // Add another entry without the name to provide search functionality on the email
+ Address emailOnly = new Address(c.getColumnKey().getEmail());
+ r.add(getReviewerByEmailFieldValue(c.getRowKey(), emailOnly));
+ }
+ r.add(v + ',' + c.getValue().getTime());
+ }
+ return r;
+ }
+
+ public static String getReviewerByEmailFieldValue(ReviewerStateInternal state, Address adr) {
+ return state.toString() + ',' + adr;
+ }
+
public static ReviewerSet parseReviewerFieldValues(Iterable<String> values) {
ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Timestamp> b =
ImmutableTable.builder();
@@ -220,6 +250,25 @@
return ReviewerSet.fromTable(b.build());
}
+ public static ReviewerByEmailSet parseReviewerByEmailFieldValues(Iterable<String> values) {
+ ImmutableTable.Builder<ReviewerStateInternal, Address, Timestamp> b = ImmutableTable.builder();
+ for (String v : values) {
+ int f = v.indexOf(',');
+ if (f < 0) {
+ continue;
+ }
+ int l = v.lastIndexOf(',');
+ if (l == f) {
+ continue;
+ }
+ b.put(
+ ReviewerStateInternal.valueOf(v.substring(0, f)),
+ Address.parse(v.substring(f + 1, l)),
+ new Timestamp(Long.valueOf(v.substring(l + 1, v.length()))));
+ }
+ return ReviewerByEmailSet.fromTable(b.build());
+ }
+
/** Commit ID of any patch set on the change, using prefix match. */
public static final FieldDef<ChangeData, Iterable<String>> COMMIT =
prefix(ChangeQueryBuilder.FIELD_COMMIT).buildRepeatable(ChangeField::getRevisions);
@@ -338,16 +387,11 @@
public static final FieldDef<ChangeData, Iterable<String>> COMMENT =
fullText(ChangeQueryBuilder.FIELD_COMMENT)
.buildRepeatable(
- cd -> {
- Set<String> r = new HashSet<>();
- for (Comment c : cd.publishedComments()) {
- r.add(c.message);
- }
- for (ChangeMessage m : cd.messages()) {
- r.add(m.getMessage());
- }
- return r;
- });
+ cd ->
+ Stream.concat(
+ cd.publishedComments().stream().map(c -> c.message),
+ cd.messages().stream().map(ChangeMessage::getMessage))
+ .collect(toSet()));
/** Number of unresolved comments of the change. */
public static final FieldDef<ChangeData, Integer> UNRESOLVED_COMMENT_COUNT =
@@ -385,22 +429,21 @@
intRange(ChangeQueryBuilder.FIELD_DELTA)
.build(cd -> cd.changedLines().map(c -> c.insertions + c.deletions).orElse(null));
+ /** Determines if this change is private. */
+ public static final FieldDef<ChangeData, String> PRIVATE =
+ exact(ChangeQueryBuilder.FIELD_PRIVATE).build(cd -> cd.change().isPrivate() ? "1" : "0");
+
/** Users who have commented on this change. */
public static final FieldDef<ChangeData, Iterable<Integer>> COMMENTBY =
integer(ChangeQueryBuilder.FIELD_COMMENTBY)
.buildRepeatable(
- cd -> {
- Set<Integer> r = new HashSet<>();
- for (ChangeMessage m : cd.messages()) {
- if (m.getAuthor() != null) {
- r.add(m.getAuthor().get());
- }
- }
- for (Comment c : cd.publishedComments()) {
- r.add(c.author.getId().get());
- }
- return r;
- });
+ cd ->
+ Stream.concat(
+ cd.messages().stream().map(ChangeMessage::getAuthor),
+ cd.publishedComments().stream().map(c -> c.author.getId()))
+ .filter(Objects::nonNull)
+ .map(Account.Id::get)
+ .collect(toSet()));
/** Star labels on this change in the format: <account-id>:<label> */
public static final FieldDef<ChangeData, Iterable<String>> STAR =
@@ -423,13 +466,8 @@
public static final FieldDef<ChangeData, Iterable<String>> GROUP =
exact(ChangeQueryBuilder.FIELD_GROUP)
.buildRepeatable(
- cd -> {
- Set<String> r = Sets.newHashSetWithExpectedSize(1);
- for (PatchSet ps : cd.patchSets()) {
- r.addAll(ps.getGroups());
- }
- return r;
- });
+ cd ->
+ cd.patchSets().stream().flatMap(ps -> ps.getGroups().stream()).collect(toSet()));
public static final ProtobufCodec<PatchSet> PATCH_SET_CODEC =
CodecFactory.encoder(PatchSet.class);
@@ -469,11 +507,7 @@
if (reviewedBy.isEmpty()) {
return ImmutableSet.of(NOT_REVIEWED);
}
- List<Integer> result = new ArrayList<>(reviewedBy.size());
- for (Account.Id id : reviewedBy) {
- result.add(id.get());
- }
- return result;
+ return reviewedBy.stream().map(Account.Id::get).collect(toList());
});
// Submit rule options in this class should never use fastEvalLabels. This
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index d988612..ba7c1ec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -90,7 +90,11 @@
@Deprecated
static final Schema<ChangeData> V38 = schema(V37, ChangeField.UNRESOLVED_COMMENT_COUNT);
- static final Schema<ChangeData> V39 = schema(V38);
+ @Deprecated static final Schema<ChangeData> V39 = schema(V38);
+
+ @Deprecated static final Schema<ChangeData> V40 = schema(V39, ChangeField.PRIVATE);
+
+ static final Schema<ChangeData> V41 = schema(V40, ChangeField.REVIEWER_BY_EMAIL);
public static final String NAME = "changes";
public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
index ec486b5..4014102 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -32,7 +32,6 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -79,30 +78,27 @@
progress.beginTask("Reindexing groups", uuids.size());
List<ListenableFuture<?>> futures = new ArrayList<>(uuids.size());
AtomicBoolean ok = new AtomicBoolean(true);
- final AtomicInteger done = new AtomicInteger();
- final AtomicInteger failed = new AtomicInteger();
+ AtomicInteger done = new AtomicInteger();
+ AtomicInteger failed = new AtomicInteger();
Stopwatch sw = Stopwatch.createStarted();
- for (final AccountGroup.UUID uuid : uuids) {
- final String desc = "group " + uuid;
+ for (AccountGroup.UUID uuid : uuids) {
+ String desc = "group " + uuid;
ListenableFuture<?> future =
executor.submit(
- new Callable<Void>() {
- @Override
- public Void call() throws Exception {
- try {
- AccountGroup oldGroup = groupCache.get(uuid);
- if (oldGroup != null) {
- groupCache.evict(oldGroup);
- }
- index.replace(groupCache.get(uuid));
- verboseWriter.println("Reindexed " + desc);
- done.incrementAndGet();
- } catch (Exception e) {
- failed.incrementAndGet();
- throw e;
+ () -> {
+ try {
+ AccountGroup oldGroup = groupCache.get(uuid);
+ if (oldGroup != null) {
+ groupCache.evict(oldGroup);
}
- return null;
+ index.replace(groupCache.get(uuid));
+ verboseWriter.println("Reindexed " + desc);
+ done.incrementAndGet();
+ } catch (Exception e) {
+ failed.incrementAndGet();
+ throw e;
}
+ return null;
});
addErrorListener(future, desc, progress, ok);
futures.add(future);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
index f3b08fb..7f3ac01 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -42,6 +42,14 @@
throw new IllegalArgumentException("Invalid email address: " + in);
}
+ public static Address tryParse(String in) {
+ try {
+ return parse(in);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
final String name;
final String email;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index bc09488..18fc083 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -180,6 +180,18 @@
setHeader("X-Gerrit-Change-Number", "" + change.getChangeId());
setChangeUrlHeader();
setCommitIdHeader();
+
+ if (notify.ordinal() >= NotifyHandling.OWNER_REVIEWERS.ordinal()) {
+ try {
+ addByEmail(
+ RecipientType.CC, changeData.reviewersByEmail().byState(ReviewerStateInternal.CC));
+ addByEmail(
+ RecipientType.TO,
+ changeData.reviewersByEmail().byState(ReviewerStateInternal.REVIEWER));
+ } catch (OrmException e) {
+ throw new EmailException("Failed to add unregistered CCs " + change.getChangeId(), e);
+ }
+ }
}
private void setChangeUrlHeader() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
index 40b2e8a..d00c905 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -588,6 +588,7 @@
footers.add("Gerrit-Comment-Date: " + getCommentTimestamp());
footers.add("Gerrit-HasComments: " + (hasComments ? "Yes" : "No"));
+ footers.add("Gerrit-HasLabels: " + (labels.isEmpty() ? "No" : "Yes"));
}
private String getLine(PatchFile fileInfo, short side, int lineNbr) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
index 3e9e62c..2c6e655 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
@@ -50,7 +50,7 @@
boolean isDraft = change.getStatus() == Change.Status.DRAFT;
try {
// Try to mark interested owners with TO and CC or BCC line.
- Watchers matching = getWatchers(NotifyType.NEW_CHANGES, !isDraft);
+ Watchers matching = getWatchers(NotifyType.NEW_CHANGES, !isDraft && !change.isPrivate());
for (Account.Id user :
Iterables.concat(matching.to.accounts, matching.cc.accounts, matching.bcc.accounts)) {
if (isOwnerOfProjectOrBranch(user)) {
@@ -69,7 +69,7 @@
log.warn("Cannot notify watchers for new change", err);
}
- includeWatchers(NotifyType.NEW_PATCHSETS, !isDraft);
+ includeWatchers(NotifyType.NEW_PATCHSETS, !isDraft && !change.isPrivate());
}
private boolean isOwnerOfProjectOrBranch(Account.Id user) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
index a563846..0fea7ce 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
@@ -20,6 +20,7 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.account.WatchConfig.NotifyType;
+import com.google.gerrit.server.mail.Address;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -32,6 +33,7 @@
/** Let users know that a reviewer and possibly her review have been removed. */
public class DeleteReviewerSender extends ReplyToChangeSender {
private final Set<Account.Id> reviewers = new HashSet<>();
+ private final Set<Address> reviewersByEmail = new HashSet<>();
public interface Factory extends ReplyToChangeSender.Factory<DeleteReviewerSender> {
@Override
@@ -49,6 +51,10 @@
reviewers.addAll(cc);
}
+ public void addReviewersByEmail(Collection<Address> cc) {
+ reviewersByEmail.addAll(cc);
+ }
+
@Override
protected void init() throws EmailException {
super.init();
@@ -58,6 +64,7 @@
ccExistingReviewers();
includeWatchers(NotifyType.ALL_COMMENTS);
add(RecipientType.TO, reviewers);
+ addByEmail(RecipientType.TO, reviewersByEmail);
removeUsersThatIgnoredTheChange();
}
@@ -70,13 +77,16 @@
}
public List<String> getReviewerNames() {
- if (reviewers.isEmpty()) {
+ if (reviewers.isEmpty() && reviewersByEmail.isEmpty()) {
return null;
}
List<String> names = new ArrayList<>();
for (Account.Id id : reviewers) {
names.add(getNameFor(id));
}
+ for (Address a : reviewersByEmail) {
+ names.add(a.toString());
+ }
return names;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NewChangeSender.java
index f1a9ad8..3f6d991 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NewChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -17,6 +17,7 @@
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import java.util.ArrayList;
@@ -28,7 +29,9 @@
/** Sends an email alerting a user to a new change for them to review. */
public abstract class NewChangeSender extends ChangeEmail {
private final Set<Account.Id> reviewers = new HashSet<>();
+ private final Set<Address> reviewersByEmail = new HashSet<>();
private final Set<Account.Id> extraCC = new HashSet<>();
+ private final Set<Address> extraCCByEmail = new HashSet<>();
protected NewChangeSender(EmailArguments ea, ChangeData cd) throws OrmException {
super(ea, "newchange", cd);
@@ -38,10 +41,18 @@
reviewers.addAll(cc);
}
+ public void addReviewersByEmail(final Collection<Address> cc) {
+ reviewersByEmail.addAll(cc);
+ }
+
public void addExtraCC(final Collection<Account.Id> cc) {
extraCC.addAll(cc);
}
+ public void addExtraCCByEmail(final Collection<Address> cc) {
+ extraCCByEmail.addAll(cc);
+ }
+
@Override
protected void init() throws EmailException {
super.init();
@@ -55,9 +66,11 @@
case ALL:
default:
add(RecipientType.CC, extraCC);
+ extraCCByEmail.stream().forEach(cc -> add(RecipientType.CC, cc));
//$FALL-THROUGH$
case OWNER_REVIEWERS:
add(RecipientType.TO, reviewers);
+ addByEmail(RecipientType.TO, reviewersByEmail);
break;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 730b710..6877879 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -441,6 +441,13 @@
}
}
+ /** Schedule this message for delivery to the listed address. */
+ protected void addByEmail(final RecipientType rt, final Collection<Address> list) {
+ for (final Address id : list) {
+ add(rt, id);
+ }
+ }
+
protected void add(final RecipientType rt, final UserIdentity who) {
if (who != null && who.getAccount() != null) {
add(rt, who.getAccount());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
index c90000f..0902d0b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
@@ -66,7 +66,8 @@
add(RecipientType.CC, extraCC);
rcptToAuthors(RecipientType.CC);
bccStarredBy();
- includeWatchers(NotifyType.NEW_PATCHSETS, !patchSet.isDraft());
+ removeUsersThatIgnoredTheChange();
+ includeWatchers(NotifyType.NEW_PATCHSETS, !patchSet.isDraft() && !change.isPrivate());
removeUsersThatIgnoredTheChange();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index 4cb570a..149f6b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -143,7 +143,7 @@
if (!read && primaryStorage == PrimaryStorage.NOTE_DB) {
throw new OrmException("NoteDb is required to read change " + changeId);
}
- boolean readOrWrite = read || args.migration.writeChanges();
+ boolean readOrWrite = read || args.migration.rawWriteChangesSetting();
if (!readOrWrite && !autoRebuild) {
loadDefaults();
return self();
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 d5b1b3d..cccf361 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
@@ -233,7 +233,7 @@
// last time this file was updated.
checkColumns(Change.Id.class, 1);
- checkColumns(Change.class, 1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18, 19, 101);
+ checkColumns(Change.class, 1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18, 19, 20, 101);
checkColumns(ChangeMessage.Key.class, 1, 2);
checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5, 6, 7);
checkColumns(PatchSet.Id.class, 1, 2);
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 c848987..a989598 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
@@ -73,6 +73,7 @@
public static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");
public static final FooterKey FOOTER_PATCH_SET_DESCRIPTION =
new FooterKey("Patch-set-description");
+ public static final FooterKey FOOTER_PRIVATE = new FooterKey("Private");
public static final FooterKey FOOTER_READ_ONLY_UNTIL = new FooterKey("Read-only-until");
public static final FooterKey FOOTER_REAL_USER = new FooterKey("Real-user");
public static final FooterKey FOOTER_STATUS = new FooterKey("Status");
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 4993a5d..ccfc762 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
@@ -48,6 +48,7 @@
import com.google.gerrit.reviewdb.client.RobotComment;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.git.RefCache;
@@ -250,9 +251,15 @@
List<ChangeNotes> notes = new ArrayList<>();
if (args.migration.enabled()) {
for (Change.Id cid : changeIds) {
- ChangeNotes cn = create(db, project, cid);
- if (cn.getChange() != null && predicate.test(cn)) {
- notes.add(cn);
+ try {
+ ChangeNotes cn = create(db, project, cid);
+ if (cn.getChange() != null && predicate.test(cn)) {
+ notes.add(cn);
+ }
+ } catch (NoSuchChangeException e) {
+ // Match ReviewDb behavior, returning not found; maybe the caller learned about it from
+ // a dangling patch set ref or something.
+ continue;
}
}
return notes;
@@ -428,6 +435,11 @@
return state.reviewers();
}
+ /** @return reviewers that do not currently have a Gerrit account and were added by email. */
+ public ReviewerByEmailSet getReviewersByEmail() {
+ return state.reviewersByEmail();
+ }
+
public ImmutableList<ReviewerStatusUpdate> getReviewerUpdates() {
return state.reviewerUpdates();
}
@@ -563,6 +575,13 @@
return state.readOnlyUntil();
}
+ public boolean isPrivate() {
+ if (state.isPrivate() == null) {
+ return false;
+ }
+ return state.isPrivate();
+ }
+
@Override
protected void onLoad(LoadHandle handle)
throws NoSuchChangeException, IOException, ConfigInvalidException {
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 dac999c..d6cccd7 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
@@ -24,6 +24,7 @@
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_PATCH_SET_DESCRIPTION;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PRIVATE;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_READ_ONLY_UNTIL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
@@ -62,8 +63,10 @@
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.gerrit.server.util.LabelVote;
import java.io.IOException;
@@ -127,6 +130,7 @@
// Private final but mutable members initialized in the constructor and filled
// in during the parsing process.
private final Table<Account.Id, ReviewerStateInternal, Timestamp> reviewers;
+ private final Table<Address, ReviewerStateInternal, Timestamp> reviewersByEmail;
private final List<Account.Id> allPastReviewers;
private final List<ReviewerStatusUpdate> reviewerUpdates;
private final List<SubmitRecord> submitRecords;
@@ -157,6 +161,7 @@
private String tag;
private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
private Timestamp readOnlyUntil;
+ private Boolean isPrivate;
ChangeNotesParser(
Change.Id changeId,
@@ -172,6 +177,7 @@
approvals = new LinkedHashMap<>();
bufferedApprovals = new ArrayList<>();
reviewers = HashBasedTable.create();
+ reviewersByEmail = HashBasedTable.create();
allPastReviewers = new ArrayList<>();
reviewerUpdates = new ArrayList<>();
submitRecords = Lists.newArrayListWithExpectedSize(1);
@@ -199,6 +205,7 @@
parseNotes();
allPastReviewers.addAll(reviewers.rowKeySet());
pruneReviewers();
+ pruneReviewersByEmail();
updatePatchSetStates();
checkMandatoryFooters();
@@ -232,13 +239,15 @@
patchSets,
buildApprovals(),
ReviewerSet.fromTable(Tables.transpose(reviewers)),
+ ReviewerByEmailSet.fromTable(Tables.transpose(reviewersByEmail)),
allPastReviewers,
buildReviewerUpdates(),
submitRecords,
buildAllMessages(),
buildMessagesByPatchSet(),
comments,
- readOnlyUntil);
+ readOnlyUntil,
+ isPrivate);
}
private PatchSet.Id buildCurrentPatchSetId() {
@@ -371,6 +380,9 @@
for (String line : commit.getFooterLineValues(state.getFooterKey())) {
parseReviewer(ts, state, line);
}
+ for (String line : commit.getFooterLineValues(state.getByEmailFooterKey())) {
+ parseReviewerByEmail(ts, state, line);
+ }
// Don't update timestamp when a reviewer was added, matching RevewDb
// behavior.
}
@@ -379,6 +391,10 @@
parseReadOnlyUntil(commit);
}
+ if (isPrivate == null) {
+ parseIsPrivate(commit);
+ }
+
if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) {
lastUpdatedOn = ts;
}
@@ -910,6 +926,19 @@
}
}
+ private void parseReviewerByEmail(Timestamp ts, ReviewerStateInternal state, String line)
+ throws ConfigInvalidException {
+ Address adr;
+ try {
+ adr = Address.parse(line);
+ } catch (IllegalArgumentException e) {
+ throw invalidFooter(state.getByEmailFooterKey(), line);
+ }
+ if (!reviewersByEmail.containsRow(adr)) {
+ reviewersByEmail.put(adr, state, ts);
+ }
+ }
+
private void parseReadOnlyUntil(ChangeNotesCommit commit) throws ConfigInvalidException {
String raw = parseOneFooter(commit, FOOTER_READ_ONLY_UNTIL);
if (raw == null) {
@@ -924,6 +953,20 @@
}
}
+ private void parseIsPrivate(ChangeNotesCommit commit) throws ConfigInvalidException {
+ String raw = parseOneFooter(commit, FOOTER_PRIVATE);
+ if (raw == null) {
+ return;
+ } else if (Boolean.TRUE.toString().equalsIgnoreCase(raw)) {
+ isPrivate = true;
+ return;
+ } else if (Boolean.FALSE.toString().equalsIgnoreCase(raw)) {
+ isPrivate = false;
+ return;
+ }
+ throw invalidFooter(FOOTER_PRIVATE, raw);
+ }
+
private void pruneReviewers() {
Iterator<Table.Cell<Account.Id, ReviewerStateInternal, Timestamp>> rit =
reviewers.cellSet().iterator();
@@ -935,6 +978,17 @@
}
}
+ private void pruneReviewersByEmail() {
+ Iterator<Table.Cell<Address, ReviewerStateInternal, Timestamp>> rit =
+ reviewersByEmail.cellSet().iterator();
+ while (rit.hasNext()) {
+ Table.Cell<Address, ReviewerStateInternal, Timestamp> e = rit.next();
+ if (e.getColumnKey() == ReviewerStateInternal.REMOVED) {
+ rit.remove();
+ }
+ }
+ }
+
private void updatePatchSetStates() {
Set<PatchSet.Id> missing = new TreeSet<>(ReviewDbUtil.intKeyOrdering());
for (Iterator<PatchSet> it = patchSets.values().iterator(); it.hasNext(); ) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 7b25bbd..eee1a34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -34,6 +34,7 @@
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.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
@@ -65,12 +66,14 @@
ImmutableList.of(),
ImmutableList.of(),
ReviewerSet.empty(),
+ ReviewerByEmailSet.empty(),
ImmutableList.of(),
ImmutableList.of(),
ImmutableList.of(),
ImmutableList.of(),
ImmutableListMultimap.of(),
ImmutableListMultimap.of(),
+ null,
null);
}
@@ -94,13 +97,15 @@
Map<PatchSet.Id, PatchSet> patchSets,
ListMultimap<PatchSet.Id, PatchSetApproval> approvals,
ReviewerSet reviewers,
+ ReviewerByEmailSet reviewersByEmail,
List<Account.Id> allPastReviewers,
List<ReviewerStatusUpdate> reviewerUpdates,
List<SubmitRecord> submitRecords,
List<ChangeMessage> allChangeMessages,
ListMultimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet,
ListMultimap<RevId, Comment> publishedComments,
- @Nullable Timestamp readOnlyUntil) {
+ @Nullable Timestamp readOnlyUntil,
+ @Nullable Boolean isPrivate) {
if (hashtags == null) {
hashtags = ImmutableSet.of();
}
@@ -119,19 +124,22 @@
originalSubject,
submissionId,
assignee,
- status),
+ status,
+ isPrivate),
ImmutableSet.copyOf(pastAssignees),
ImmutableSet.copyOf(hashtags),
ImmutableList.copyOf(patchSets.entrySet()),
ImmutableList.copyOf(approvals.entries()),
reviewers,
+ reviewersByEmail,
ImmutableList.copyOf(allPastReviewers),
ImmutableList.copyOf(reviewerUpdates),
ImmutableList.copyOf(submitRecords),
ImmutableList.copyOf(allChangeMessages),
ImmutableListMultimap.copyOf(changeMessagesByPatchSet),
ImmutableListMultimap.copyOf(publishedComments),
- readOnlyUntil);
+ readOnlyUntil,
+ isPrivate);
}
/**
@@ -174,6 +182,9 @@
// TODO(dborowitz): Use a sensible default other than null
@Nullable
abstract Change.Status status();
+
+ @Nullable
+ abstract Boolean isPrivate();
}
// Only null if NoteDb is disabled.
@@ -197,6 +208,8 @@
abstract ReviewerSet reviewers();
+ abstract ReviewerByEmailSet reviewersByEmail();
+
abstract ImmutableList<Account.Id> allPastReviewers();
abstract ImmutableList<ReviewerStatusUpdate> reviewerUpdates();
@@ -212,6 +225,9 @@
@Nullable
abstract Timestamp readOnlyUntil();
+ @Nullable
+ abstract Boolean isPrivate();
+
Change newChange(Project.NameKey project) {
ChangeColumns c = checkNotNull(columns(), "columns are required");
Change change =
@@ -269,6 +285,7 @@
change.setLastUpdatedOn(c.lastUpdatedOn());
change.setSubmissionId(c.submissionId());
change.setAssignee(c.assignee());
+ change.setPrivate(c.isPrivate() == null ? false : c.isPrivate());
if (!patchSets().isEmpty()) {
change.setCurrentPatchSet(c.currentPatchSetId(), c.subject(), c.originalSubject());
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 7af0cb4..f699d76 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
@@ -29,6 +29,7 @@
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_PATCH_SET_DESCRIPTION;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PRIVATE;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_READ_ONLY_UNTIL;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
@@ -60,6 +61,7 @@
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.util.LabelVote;
@@ -127,6 +129,7 @@
private final Table<String, Account.Id, Optional<Short>> approvals;
private final Map<Account.Id, ReviewerStateInternal> reviewers = new LinkedHashMap<>();
+ private final Map<Address, ReviewerStateInternal> reviewersByEmail = new LinkedHashMap<>();
private final List<Comment> comments = new ArrayList<>();
private String commitSubject;
@@ -149,6 +152,7 @@
private String psDescription;
private boolean currentPatchSet;
private Timestamp readOnlyUntil;
+ private Boolean isPrivate;
private ChangeDraftUpdate draftUpdate;
private RobotCommentUpdate robotCommentUpdate;
@@ -469,6 +473,15 @@
reviewers.put(reviewer, ReviewerStateInternal.REMOVED);
}
+ public void putReviewerByEmail(Address reviewer, ReviewerStateInternal type) {
+ checkArgument(type != ReviewerStateInternal.REMOVED, "invalid ReviewerType");
+ reviewersByEmail.put(reviewer, type);
+ }
+
+ public void removeReviewerByEmail(Address reviewer) {
+ reviewersByEmail.put(reviewer, ReviewerStateInternal.REMOVED);
+ }
+
public void setPatchSetState(PatchSetState psState) {
this.psState = psState;
}
@@ -658,6 +671,10 @@
addIdent(msg, e.getKey()).append('\n');
}
+ for (Map.Entry<Address, ReviewerStateInternal> e : reviewersByEmail.entrySet()) {
+ addFooter(msg, e.getValue().getByEmailFooterKey(), e.getKey().toString());
+ }
+
for (Table.Cell<String, Account.Id, Optional<Short>> c : approvals.cellSet()) {
addFooter(msg, FOOTER_LABEL);
// Label names/values are safe to append without sanitizing.
@@ -711,6 +728,10 @@
addFooter(msg, FOOTER_READ_ONLY_UNTIL, ChangeNoteUtil.formatTime(serverIdent, readOnlyUntil));
}
+ if (isPrivate != null) {
+ addFooter(msg, FOOTER_PRIVATE, isPrivate);
+ }
+
cb.setMessage(msg.toString());
try {
ObjectId treeId = storeRevisionNotes(rw, ins, curr);
@@ -743,6 +764,7 @@
&& changeMessage == null
&& comments.isEmpty()
&& reviewers.isEmpty()
+ && reviewersByEmail.isEmpty()
&& changeId == null
&& branch == null
&& status == null
@@ -757,7 +779,8 @@
&& tag == null
&& psDescription == null
&& !currentPatchSet
- && readOnlyUntil == null;
+ && readOnlyUntil == null
+ && isPrivate == null;
}
ChangeDraftUpdate getDraftUpdate() {
@@ -777,6 +800,10 @@
return isAllowWriteToNewtRef;
}
+ public void setPrivate(boolean isPrivate) {
+ this.isPrivate = isPrivate;
+ }
+
void setReadOnlyUntil(Timestamp readOnlyUntil) {
this.readOnlyUntil = readOnlyUntil;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
index c0b0525..b894393 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
@@ -84,7 +84,7 @@
private final boolean disableChangeReviewDb;
@Inject
- ConfigNotesMigration(@GerritServerConfig Config cfg) {
+ public ConfigNotesMigration(@GerritServerConfig Config cfg) {
checkConfig(cfg);
writeChanges = cfg.getBoolean(NOTE_DB, CHANGES.key(), WRITE, false);
@@ -106,7 +106,7 @@
}
@Override
- protected boolean writeChanges() {
+ public boolean rawWriteChangesSetting() {
return writeChanges;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 59d7cbb..194372f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -43,9 +43,9 @@
import com.google.gerrit.server.update.ChainedReceiveCommands;
import com.google.gwtorm.server.OrmConcurrencyException;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
@@ -207,7 +207,7 @@
private String refLogMessage;
private PersonIdent refLogIdent;
- @AssistedInject
+ @Inject
NoteDbUpdateManager(
@GerritPersonIdent Provider<PersonIdent> serverIdent,
GitRepositoryManager repoManager,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
index c708bfe..995847f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -49,6 +49,9 @@
/**
* Write changes to NoteDb.
*
+ * <p>This method is awkwardly named because you should be using either {@link
+ * #commitChangeWrites()} or {@link #failChangeWrites()} instead.
+ *
* <p>Updates to change data are written to NoteDb refs, but ReviewDb is still the source of
* truth. Change data will not be written unless the NoteDb refs are already up to date, and the
* write path will attempt to rebuild the change if not.
@@ -57,7 +60,7 @@
* readChanges() = false}, writes to NoteDb are simply ignored; if {@code true}, any attempts to
* write will generate an error.
*/
- protected abstract boolean writeChanges();
+ public abstract boolean rawWriteChangesSetting();
/**
* Read sequential change ID numbers from NoteDb.
@@ -99,14 +102,14 @@
// same codepath. This specific condition is used by the auto-rebuilding
// path to rebuild a change and stage the results, but not commit them due
// to failChangeWrites().
- return writeChanges() || readChanges();
+ return rawWriteChangesSetting() || readChanges();
}
public boolean failChangeWrites() {
- return !writeChanges() && readChanges();
+ return !rawWriteChangesSetting() && readChanges();
}
public boolean enabled() {
- return writeChanges() || readChanges();
+ return rawWriteChangesSetting() || readChanges();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
index f250646..fad9832 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
@@ -29,13 +29,17 @@
/** The user was previously a reviewer on the change, but was removed. */
REMOVED(new FooterKey("Removed"), ReviewerState.REMOVED);
+ public static ReviewerStateInternal fromReviewerState(ReviewerState state) {
+ return ReviewerStateInternal.values()[state.ordinal()];
+ }
+
static {
boolean ok = true;
if (ReviewerStateInternal.values().length != ReviewerState.values().length) {
ok = false;
}
- for (ReviewerStateInternal s : ReviewerStateInternal.values()) {
- ok &= s.name().equals(s.state.name());
+ for (int i = 0; i < ReviewerStateInternal.values().length; i++) {
+ ok &= ReviewerState.values()[i].equals(ReviewerStateInternal.values()[i].state);
}
if (!ok) {
throw new IllegalStateException(
@@ -58,6 +62,10 @@
return footerKey;
}
+ FooterKey getByEmailFooterKey() {
+ return new FooterKey(footerKey.getName() + "-email");
+ }
+
public ReviewerState asReviewerState() {
return state;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
index e6549f0..99d9615 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
@@ -24,8 +24,8 @@
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.client.RobotComment;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
@@ -44,7 +44,7 @@
private RevisionNoteMap<RobotCommentsRevisionNote> revisionNoteMap;
private ObjectId metaId;
- @AssistedInject
+ @Inject
RobotCommentNotes(Args args, @Assisted Change change) {
super(args, change.getId(), PrimaryStorage.of(change), false);
this.change = change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
index 6f9090f..8ce9987 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
@@ -25,7 +25,6 @@
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import java.io.IOException;
-import java.util.concurrent.Callable;
public abstract class ChangeRebuilder {
public static class NoPatchSetsException extends OrmException {
@@ -43,14 +42,11 @@
}
public final ListenableFuture<Result> rebuildAsync(
- final Change.Id id, ListeningExecutorService executor) {
+ Change.Id id, ListeningExecutorService executor) {
return executor.submit(
- new Callable<Result>() {
- @Override
- public Result call() throws Exception {
- try (ReviewDb db = schemaFactory.open()) {
- return rebuild(db, id);
- }
+ () -> {
+ try (ReviewDb db = schemaFactory.open()) {
+ return rebuild(db, id);
}
});
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
index fa02691..188513f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
@@ -16,8 +16,8 @@
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -36,7 +36,7 @@
private final DiffSummaryKey key;
private final Project.NameKey project;
- @AssistedInject
+ @Inject
DiffSummaryLoader(PatchListCache plc, @Assisted DiffSummaryKey k, @Assisted Project.NameKey p) {
patchListCache = plc;
key = k;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index a571c46..54d9540 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -18,8 +18,8 @@
import com.google.common.base.Throwables;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@@ -52,7 +52,7 @@
private final IntraLineDiffKey key;
private final IntraLineDiffArgs args;
- @AssistedInject
+ @Inject
IntraLineLoader(
@DiffExecutor ExecutorService diffExecutor,
@GerritServerConfig Config cfg,
@@ -75,12 +75,7 @@
public IntraLineDiff call() throws Exception {
Future<IntraLineDiff> result =
diffExecutor.submit(
- new Callable<IntraLineDiff>() {
- @Override
- public IntraLineDiff call() throws Exception {
- return IntraLineLoader.compute(args.aText(), args.bText(), args.edits());
- }
- });
+ () -> IntraLineLoader.compute(args.aText(), args.bText(), args.edits()));
try {
return result.get(timeoutMillis, TimeUnit.MILLISECONDS);
} catch (InterruptedException | TimeoutException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 124fe8e..b766a02 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -29,8 +29,8 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.git.MergeUtil;
+import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
@@ -86,7 +86,7 @@
private final long timeoutMillis;
private final boolean save;
- @AssistedInject
+ @Inject
PatchListLoader(
GitRepositoryManager mgr,
PatchListCache plc,
@@ -250,17 +250,13 @@
}
private FileHeader toFileHeader(
- PatchListKey key, final DiffFormatter diffFormatter, final DiffEntry diffEntry)
- throws IOException {
+ PatchListKey key, DiffFormatter diffFormatter, DiffEntry diffEntry) throws IOException {
Future<FileHeader> result =
diffExecutor.submit(
- new Callable<FileHeader>() {
- @Override
- public FileHeader call() throws IOException {
- synchronized (diffEntry) {
- return diffFormatter.toFileHeader(diffEntry);
- }
+ () -> {
+ synchronized (diffEntry) {
+ return diffFormatter.toFileHeader(diffEntry);
}
});
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 82c6150..9e80f38 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -253,9 +253,7 @@
return b;
}
- private ObjectId toObjectId(PatchSet ps)
- throws NoSuchChangeException, AuthException, NoSuchChangeException, IOException,
- OrmException {
+ private ObjectId toObjectId(PatchSet ps) throws AuthException, IOException, OrmException {
if (ps.getId().get() == 0) {
return getEditRev();
}
@@ -271,11 +269,10 @@
}
}
- private ObjectId getEditRev()
- throws AuthException, NoSuchChangeException, IOException, OrmException {
+ private ObjectId getEditRev() throws AuthException, IOException, OrmException {
edit = editReader.byChange(change);
if (edit.isPresent()) {
- return edit.get().getRef().getObjectId();
+ return edit.get().getEditCommit();
}
throw new NoSuchChangeException(change.getId());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermission.java
new file mode 100644
index 0000000..4b06861
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermission.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import com.google.gerrit.common.data.Permission;
+import java.util.Locale;
+import java.util.Optional;
+
+public enum ChangePermission implements ChangePermissionOrLabel {
+ READ(Permission.READ),
+ RESTORE,
+ DELETE,
+ ABANDON(Permission.ABANDON),
+ EDIT_ASSIGNEE(Permission.EDIT_ASSIGNEE),
+ EDIT_DESCRIPTION,
+ EDIT_HASHTAGS(Permission.EDIT_HASHTAGS),
+ EDIT_TOPIC_NAME(Permission.EDIT_TOPIC_NAME),
+ REMOVE_REVIEWER(Permission.REMOVE_REVIEWER),
+ ADD_PATCH_SET(Permission.ADD_PATCH_SET),
+ REBASE(Permission.REBASE),
+ SUBMIT(Permission.SUBMIT),
+ SUBMIT_AS(Permission.SUBMIT_AS);
+
+ private final String name;
+
+ ChangePermission() {
+ name = null;
+ }
+
+ ChangePermission(String name) {
+ this.name = name;
+ }
+
+ /** @return name used in {@code project.config} permissions. */
+ @Override
+ public Optional<String> permissionName() {
+ return Optional.ofNullable(name);
+ }
+
+ @Override
+ public String describeForException() {
+ return toString().toLowerCase(Locale.US).replace('_', ' ');
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java
new file mode 100644
index 0000000..06c0d73
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import java.util.Optional;
+
+/** A {@link ChangePermission} or a {@link LabelPermission}. */
+public interface ChangePermissionOrLabel {
+ /** @return name used in {@code project.config} permissions. */
+ public Optional<String> permissionName();
+
+ /** @return readable identifier of this permission for exception message. */
+ public String describeForException();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
new file mode 100644
index 0000000..4945879
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -0,0 +1,168 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
+import com.google.gerrit.server.permissions.PermissionBackend.ForProject;
+import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Provider;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Helpers for {@link PermissionBackend} that must fail.
+ *
+ * <p>These helpers are useful to curry failure state identified inside a non-throwing factory
+ * method to the throwing {@code check} or {@code test} methods.
+ */
+public class FailedPermissionBackend {
+ public static ForProject project(String message) {
+ return project(message, null);
+ }
+
+ public static ForProject project(String message, Throwable cause) {
+ return new FailedProject(message, cause);
+ }
+
+ public static ForRef ref(String message) {
+ return ref(message, null);
+ }
+
+ public static ForRef ref(String message, Throwable cause) {
+ return new FailedRef(message, cause);
+ }
+
+ public static ForChange change(String message) {
+ return change(message, null);
+ }
+
+ public static ForChange change(String message, Throwable cause) {
+ return new FailedChange(message, cause);
+ }
+
+ private FailedPermissionBackend() {}
+
+ private static class FailedProject extends ForProject {
+ private final String message;
+ private final Throwable cause;
+
+ FailedProject(String message, Throwable cause) {
+ this.message = message;
+ this.cause = cause;
+ }
+
+ @Override
+ public ForProject database(Provider<ReviewDb> db) {
+ return this;
+ }
+
+ @Override
+ public ForProject user(CurrentUser user) {
+ return this;
+ }
+
+ @Override
+ public ForRef ref(String ref) {
+ return new FailedRef(message, cause);
+ }
+
+ @Override
+ public void check(ProjectPermission perm) throws PermissionBackendException {
+ throw new PermissionBackendException(message, cause);
+ }
+
+ @Override
+ public Set<ProjectPermission> test(Collection<ProjectPermission> permSet)
+ throws PermissionBackendException {
+ throw new PermissionBackendException(message, cause);
+ }
+ }
+
+ private static class FailedRef extends ForRef {
+ private final String message;
+ private final Throwable cause;
+
+ FailedRef(String message, Throwable cause) {
+ this.message = message;
+ this.cause = cause;
+ }
+
+ @Override
+ public ForRef database(Provider<ReviewDb> db) {
+ return this;
+ }
+
+ @Override
+ public ForRef user(CurrentUser user) {
+ return this;
+ }
+
+ @Override
+ public ForChange change(ChangeData cd) {
+ return new FailedChange(message, cause);
+ }
+
+ @Override
+ public ForChange change(ChangeNotes cd) {
+ return new FailedChange(message, cause);
+ }
+
+ @Override
+ public void check(RefPermission perm) throws PermissionBackendException {
+ throw new PermissionBackendException(message, cause);
+ }
+
+ @Override
+ public Set<RefPermission> test(Collection<RefPermission> permSet)
+ throws PermissionBackendException {
+ throw new PermissionBackendException(message, cause);
+ }
+ }
+
+ private static class FailedChange extends ForChange {
+ private final String message;
+ private final Throwable cause;
+
+ FailedChange(String message, Throwable cause) {
+ this.message = message;
+ this.cause = cause;
+ }
+
+ @Override
+ public ForChange database(Provider<ReviewDb> db) {
+ return this;
+ }
+
+ @Override
+ public ForChange user(CurrentUser user) {
+ return this;
+ }
+
+ @Override
+ public void check(ChangePermissionOrLabel perm) throws PermissionBackendException {
+ throw new PermissionBackendException(message, cause);
+ }
+
+ @Override
+ public <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet)
+ throws PermissionBackendException {
+ throw new PermissionBackendException(message, cause);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java
new file mode 100644
index 0000000..61f7330
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.server.util.LabelVote;
+import java.util.Optional;
+
+/** Permission representing a label. */
+public class LabelPermission implements ChangePermissionOrLabel {
+ private final String name;
+
+ /**
+ * Construct a reference to a label permission.
+ *
+ * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
+ */
+ public LabelPermission(String name) {
+ this.name = LabelType.checkName(name);
+ }
+
+ /** @return name of the label, e.g. {@code "Code-Review"}. */
+ public String label() {
+ return name;
+ }
+
+ /** @return name used in {@code project.config} permissions. */
+ @Override
+ public Optional<String> permissionName() {
+ return Optional.of(Permission.forLabel(label()));
+ }
+
+ @Override
+ public String describeForException() {
+ return "label " + label();
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof LabelPermission && name.equals(((LabelPermission) other).name);
+ }
+
+ @Override
+ public String toString() {
+ return "Label[" + name + ']';
+ }
+
+ /** A {@link LabelPermission} at a specific value. */
+ public static class WithValue implements ChangePermissionOrLabel {
+ private final LabelVote label;
+
+ /**
+ * Construct a reference to a label at a specific value.
+ *
+ * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
+ * @param value numeric score assigned to the label.
+ */
+ public WithValue(String name, short value) {
+ this(LabelVote.create(name, value));
+ }
+
+ /**
+ * Construct a reference to a label at a specific value.
+ *
+ * @param label label name and vote.
+ */
+ public WithValue(LabelVote label) {
+ this.label = checkNotNull(label, "LabelVote");
+ }
+
+ /** @return name of the label, e.g. {@code "Code-Review"}. */
+ public String label() {
+ return label.label();
+ }
+
+ /** @return specific value of the label, e.g. 1 or 2. */
+ public short value() {
+ return label.value();
+ }
+
+ /** @return name used in {@code project.config} permissions. */
+ @Override
+ public Optional<String> permissionName() {
+ return Optional.of(Permission.forLabel(label()));
+ }
+
+ @Override
+ public String describeForException() {
+ return "label " + label.formatWithEquals();
+ }
+
+ @Override
+ public int hashCode() {
+ return label.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other instanceof WithValue && label.equals(((WithValue) other).label);
+ }
+
+ @Override
+ public String toString() {
+ return "Label[" + label.format() + ']';
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
new file mode 100644
index 0000000..9e5350b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -0,0 +1,211 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Checks authorization to perform an action on project, ref, or change.
+ *
+ * <p>{@code PermissionBackend} should be a singleton for the server, acting as a factory for
+ * lightweight request instances.
+ *
+ * <p>{@code check} methods should be used during action handlers to verify the user is allowed to
+ * exercise the specified permission. For convenience in implementation {@code check} methods throw
+ * {@link AuthException} if the permission is denied.
+ *
+ * <p>{@code test} methods should be used when constructing replies to the client and the result
+ * object needs to include a true/false hint indicating the user's ability to exercise the
+ * permission. This is suitable for configuring UI button state, but should not be relied upon to
+ * guard handlers before making state changes.
+ *
+ * <p>Example use:
+ *
+ * <pre>
+ * private final PermissionBackend permissions;
+ * private final Provider<CurrentUser> user;
+ *
+ * @Inject
+ * Foo(PermissionBackend permissions, Provider<CurrentUser> user) {
+ * this.permissions = permissions;
+ * this.user = user;
+ * }
+ *
+ * public void apply(...) {
+ * permissions.user(user).change(cd).check(ChangePermission.SUBMIT);
+ * }
+ *
+ * public UiAction.Description getDescription(ChangeResource rsrc) {
+ * return new UiAction.Description()
+ * .setLabel("Submit")
+ * .setVisible(rsrc.permissions().testOrFalse(ChangePermission.SUBMIT));
+ * }
+ * </pre>
+ */
+public abstract class PermissionBackend {
+ private static final Logger logger = LoggerFactory.getLogger(PermissionBackend.class);
+
+ /** @return lightweight factory scoped to answer for the specified user. */
+ public abstract WithUser user(CurrentUser user);
+
+ /** @return lightweight factory scoped to answer for the specified user. */
+ public WithUser user(Provider<CurrentUser> user) {
+ return user(checkNotNull(user, "Provider<CurrentUser>").get());
+ }
+
+ /** PermissionBackend with an optional per-request ReviewDb handle. */
+ public abstract static class AcceptsReviewDb<T> {
+ protected Provider<ReviewDb> db;
+
+ public T database(Provider<ReviewDb> db) {
+ if (db != null) {
+ this.db = db;
+ }
+ return self();
+ }
+
+ public T database(ReviewDb db) {
+ return database(Providers.of(checkNotNull(db, "ReviewDb")));
+ }
+
+ @SuppressWarnings("unchecked")
+ private T self() {
+ return (T) this;
+ }
+ }
+
+ /** PermissionBackend scoped to a specific user. */
+ public abstract static class WithUser extends AcceptsReviewDb<WithUser> {
+ /** @return instance scoped for the specified project. */
+ public abstract ForProject project(Project.NameKey project);
+
+ /** @return instance scoped for the {@code ref}, and its parent project. */
+ public ForRef ref(Branch.NameKey ref) {
+ return project(ref.getParentKey()).ref(ref.get()).database(db);
+ }
+
+ /** @return instance scoped for the change, and its destination ref and project. */
+ public ForChange change(ChangeData cd) {
+ try {
+ return ref(cd.change().getDest()).change(cd);
+ } catch (OrmException e) {
+ return FailedPermissionBackend.change("unavailable", e);
+ }
+ }
+
+ /** @return instance scoped for the change, and its destination ref and project. */
+ public ForChange change(ChangeNotes notes) {
+ return ref(notes.getChange().getDest()).change(notes);
+ }
+ }
+
+ /** PermissionBackend scoped to a user and project. */
+ public abstract static class ForProject extends AcceptsReviewDb<ForProject> {
+ /** @return new instance rescoped to same project, but different {@code user}. */
+ public abstract ForProject user(CurrentUser user);
+
+ /** @return instance scoped for {@code ref} in this project. */
+ public abstract ForRef ref(String ref);
+
+ /** Verify scoped user can {@code perm}, throwing if denied. */
+ public abstract void check(ProjectPermission perm)
+ throws AuthException, PermissionBackendException;
+
+ /** Filter {@code permSet} to permissions scoped user might be able to perform. */
+ public abstract Set<ProjectPermission> test(Collection<ProjectPermission> permSet)
+ throws PermissionBackendException;
+
+ public boolean test(ProjectPermission perm) throws PermissionBackendException {
+ return test(EnumSet.of(perm)).contains(perm);
+ }
+ }
+
+ /** PermissionBackend scoped to a user, project and reference. */
+ public abstract static class ForRef extends AcceptsReviewDb<ForRef> {
+ /** @return new instance rescoped to same reference, but different {@code user}. */
+ public abstract ForRef user(CurrentUser user);
+
+ /** @return instance scoped to change. */
+ public abstract ForChange change(ChangeData cd);
+
+ /** @return instance scoped to change. */
+ public abstract ForChange change(ChangeNotes notes);
+
+ /** Verify scoped user can {@code perm}, throwing if denied. */
+ public abstract void check(RefPermission perm) throws AuthException, PermissionBackendException;
+
+ /** Filter {@code permSet} to permissions scoped user might be able to perform. */
+ public abstract Set<RefPermission> test(Collection<RefPermission> permSet)
+ throws PermissionBackendException;
+
+ public boolean test(RefPermission perm) throws PermissionBackendException {
+ return test(EnumSet.of(perm)).contains(perm);
+ }
+ }
+
+ /** PermissionBackend scoped to a user, project, reference and change. */
+ public abstract static class ForChange extends AcceptsReviewDb<ForChange> {
+ /** @return new instance rescoped to same change, but different {@code user}. */
+ public abstract ForChange user(CurrentUser user);
+
+ /** Verify scoped user can {@code perm}, throwing if denied. */
+ public abstract void check(ChangePermissionOrLabel perm)
+ throws AuthException, PermissionBackendException;
+
+ /** Filter {@code permSet} to permissions scoped user might be able to perform. */
+ public abstract <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet)
+ throws PermissionBackendException;
+
+ public boolean test(ChangePermissionOrLabel perm) throws PermissionBackendException {
+ return test(Collections.singleton(perm)).contains(perm);
+ }
+
+ /**
+ * Test if user may be able to perform the permission.
+ *
+ * <p>Similar to {@link #test(ChangePermissionOrLabel)} except this method returns {@code false}
+ * instead of throwing an exception.
+ *
+ * @param perm the permission to test.
+ * @return true if the user might be able to perform the permission; false if the user may be
+ * missing the necessary grants or state, or if the backend threw an exception.
+ */
+ public boolean testOrFalse(ChangePermissionOrLabel perm) {
+ try {
+ return test(perm);
+ } catch (PermissionBackendException e) {
+ logger.warn("Cannot test " + perm + "; assuming false", e);
+ return false;
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackendException.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackendException.java
new file mode 100644
index 0000000..be02a6f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackendException.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.server.account.GroupBackend;
+
+/**
+ * Thrown when {@link PermissionBackend} cannot compute the result.
+ *
+ * <p>This is typically a transient failure, such as a required {@link GroupBackend} not responding
+ * to membership requests.
+ */
+public class PermissionBackendException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ public PermissionBackendException(String message) {
+ super(message);
+ }
+
+ public PermissionBackendException(@Nullable Throwable cause) {
+ super(cause);
+ }
+
+ public PermissionBackendException(String message, @Nullable Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java
new file mode 100644
index 0000000..81c38f4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import com.google.gerrit.common.data.Permission;
+import java.util.Locale;
+import java.util.Optional;
+
+public enum ProjectPermission {
+ READ(Permission.READ);
+
+ private final String name;
+
+ ProjectPermission() {
+ name = null;
+ }
+
+ ProjectPermission(String name) {
+ this.name = name;
+ }
+
+ /** @return name used in {@code project.config} permissions. */
+ public Optional<String> permissionName() {
+ return Optional.ofNullable(name);
+ }
+
+ public String describeForException() {
+ return toString().toLowerCase(Locale.US).replace('_', ' ');
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java
new file mode 100644
index 0000000..37744b0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import com.google.gerrit.common.data.Permission;
+import java.util.Locale;
+import java.util.Optional;
+
+public enum RefPermission {
+ READ(Permission.READ),
+ CREATE(Permission.CREATE),
+ DELETE(Permission.DELETE),
+ UPDATE(Permission.PUSH),
+ FORCE_UPDATE,
+
+ FORGE_AUTHOR(Permission.FORGE_AUTHOR),
+ FORGE_COMMITTER(Permission.FORGE_COMMITTER),
+ FORGE_SERVER(Permission.FORGE_SERVER),
+
+ CREATE_CHANGE;
+
+ private final String name;
+
+ RefPermission() {
+ name = null;
+ }
+
+ RefPermission(String name) {
+ this.name = name;
+ }
+
+ /** @return name used in {@code project.config} permissions. */
+ public Optional<String> permissionName() {
+ return Optional.ofNullable(name);
+ }
+
+ public String describeForException() {
+ return toString().toLowerCase(Locale.US).replace('_', ' ');
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 957bdd7..d790fe9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -404,7 +404,7 @@
String name = entry.getKey();
Path path = entry.getValue();
String fileName = path.getFileName().toString();
- if (!isJsPlugin(fileName) && !serverPluginFactory.handles(path)) {
+ if (!isUiPlugin(fileName) && !serverPluginFactory.handles(path)) {
log.warn("No Plugin provider was found that handles this file format: {}", fileName);
continue;
}
@@ -586,7 +586,7 @@
private Plugin loadPlugin(String name, Path srcPlugin, FileSnapshot snapshot)
throws InvalidPluginException {
String pluginName = srcPlugin.getFileName().toString();
- if (isJsPlugin(pluginName)) {
+ if (isUiPlugin(pluginName)) {
return loadJsPlugin(name, srcPlugin, snapshot);
} else if (serverPluginFactory.handles(srcPlugin)) {
return loadServerPlugin(srcPlugin, snapshot);
@@ -718,8 +718,8 @@
public String getGerritPluginName(Path srcPath) {
String fileName = srcPath.getFileName().toString();
- if (isJsPlugin(fileName)) {
- return fileName.substring(0, fileName.length() - 3);
+ if (isUiPlugin(fileName)) {
+ return fileName.substring(0, fileName.lastIndexOf('.'));
}
if (serverPluginFactory.handles(srcPath)) {
return serverPluginFactory.getPluginName(srcPath);
@@ -735,8 +735,8 @@
return map;
}
- private static boolean isJsPlugin(String name) {
- return isPlugin(name, "js");
+ private static boolean isUiPlugin(String name) {
+ return isPlugin(name, "js") || isPlugin(name, "html");
}
private static boolean isPlugin(String fileName, String ext) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
index 5c0d8d7..6d77267 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
@@ -46,8 +46,5 @@
.annotatedWith(GitReceivePackGroups.class)
.toProvider(GitReceivePackGroupsProvider.class)
.in(SINGLETON);
-
- bind(ChangeControl.Factory.class);
- factory(ProjectControl.AssistedFactory.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index ec114d8..af89d94 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -15,13 +15,17 @@
package com.google.gerrit.server.project;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
@@ -32,13 +36,22 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.ChangePermissionOrLabel;
+import com.google.gerrit.server.permissions.LabelPermission;
+import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
+import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
+import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
+import java.util.EnumSet;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
/** Access control management for a user accessing a single change. */
public class ChangeControl {
@@ -200,6 +213,9 @@
/** Can this user see this change? */
public boolean isVisible(ReviewDb db, @Nullable ChangeData cd) throws OrmException {
+ if (getChange().isPrivate() && !isPrivateVisible(db, cd)) {
+ return false;
+ }
if (getChange().getStatus() == Change.Status.DRAFT && !isDraftVisible(db, cd)) {
return false;
}
@@ -230,7 +246,7 @@
}
/** Can this user abandon this change? */
- public boolean canAbandon(ReviewDb db) throws OrmException {
+ private boolean canAbandon(ReviewDb db) throws OrmException {
return (isOwner() // owner (aka creator) of the change can abandon
|| getRefControl().isOwner() // branch owner can abandon
|| getProjectControl().isOwner() // project owner can abandon
@@ -269,13 +285,13 @@
}
/** Can this user rebase this change? */
- public boolean canRebase(ReviewDb db) throws OrmException {
+ private boolean canRebase(ReviewDb db) throws OrmException {
return (isOwner() || getRefControl().canSubmit(isOwner()) || getRefControl().canRebase())
&& !isPatchSetLocked(db);
}
/** Can this user restore this change? */
- public boolean canRestore(ReviewDb db) throws OrmException {
+ private boolean canRestore(ReviewDb db) throws OrmException {
return canAbandon(db) // Anyone who can abandon the change can restore it back
&& getRefControl().canUpload(); // as long as you can upload too
}
@@ -416,7 +432,7 @@
}
/** Can this user edit the topic name? */
- public boolean canEditTopicName() {
+ private boolean canEditTopicName() {
if (getChange().getStatus().isOpen()) {
return isOwner() // owner (aka creator) of the change can edit topic
|| getRefControl().isOwner() // branch owner can edit topic
@@ -429,7 +445,7 @@
}
/** Can this user edit the description? */
- public boolean canEditDescription() {
+ private boolean canEditDescription() {
if (getChange().getStatus().isOpen()) {
return isOwner() // owner (aka creator) of the change can edit desc
|| getRefControl().isOwner() // branch owner can edit desc
@@ -440,7 +456,7 @@
return false;
}
- public boolean canEditAssignee() {
+ private boolean canEditAssignee() {
return isOwner()
|| getProjectControl().isOwner()
|| getRefControl().canEditAssignee()
@@ -448,7 +464,7 @@
}
/** Can this user edit the hashtag name? */
- public boolean canEditHashtags() {
+ private boolean canEditHashtags() {
return isOwner() // owner (aka creator) of the change can edit hashtags
|| getRefControl().isOwner() // branch owner can edit hashtags
|| getProjectControl().isOwner() // project owner can edit hashtags
@@ -456,14 +472,6 @@
|| getRefControl().canEditHashtags(); // user can edit hashtag on a specific ref
}
- public boolean canSubmit() {
- return getRefControl().canSubmit(isOwner());
- }
-
- public boolean canSubmitAs() {
- return getRefControl().canSubmitAs();
- }
-
private boolean match(String destBranch, String refPattern) {
return RefPatternMatcher.getMatcher(refPattern).match(destBranch, getUser());
}
@@ -478,4 +486,146 @@
|| getRefControl().canViewDrafts()
|| getUser().isInternalUser();
}
+
+ public boolean isPrivateVisible(ReviewDb db, ChangeData cd) throws OrmException {
+ return isOwner()
+ || isReviewer(db, cd)
+ || getRefControl().canViewPrivateChanges()
+ || getUser().isInternalUser();
+ }
+
+ ForChange asForChange(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
+ return new ForChangeImpl(cd, db);
+ }
+
+ private class ForChangeImpl extends ForChange {
+ private ChangeData cd;
+ private Map<String, PermissionRange> labels;
+
+ ForChangeImpl(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
+ this.cd = cd;
+ this.db = db;
+ }
+
+ private ReviewDb db() {
+ if (db != null) {
+ return db.get();
+ } else if (cd != null) {
+ return cd.db();
+ } else {
+ return null;
+ }
+ }
+
+ private ChangeData changeData() {
+ if (cd == null) {
+ ReviewDb reviewDb = db();
+ checkState(reviewDb != null, "need ReviewDb");
+ cd = changeDataFactory.create(reviewDb, ChangeControl.this);
+ }
+ return cd;
+ }
+
+ @Override
+ public ForChange user(CurrentUser user) {
+ return getUser().equals(user) ? this : forUser(user).asForChange(cd, db);
+ }
+
+ @Override
+ public void check(ChangePermissionOrLabel perm)
+ throws AuthException, PermissionBackendException {
+ if (!can(perm)) {
+ throw new AuthException(perm.describeForException() + " not permitted");
+ }
+ }
+
+ @Override
+ public <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet)
+ throws PermissionBackendException {
+ Set<T> ok = newSet(permSet);
+ for (T perm : permSet) {
+ if (can(perm)) {
+ ok.add(perm);
+ }
+ }
+ return ok;
+ }
+
+ private boolean can(ChangePermissionOrLabel perm) throws PermissionBackendException {
+ if (perm instanceof ChangePermission) {
+ return can((ChangePermission) perm);
+ } else if (perm instanceof LabelPermission) {
+ return can((LabelPermission) perm);
+ } else if (perm instanceof LabelPermission.WithValue) {
+ return can((LabelPermission.WithValue) perm);
+ }
+ throw new PermissionBackendException(perm + " unsupported");
+ }
+
+ private boolean can(ChangePermission perm) throws PermissionBackendException {
+ try {
+ switch (perm) {
+ case READ:
+ return isVisible(db(), changeData());
+ case ABANDON:
+ return canAbandon(db());
+ case DELETE:
+ return canDelete(db(), getChange().getStatus());
+ case ADD_PATCH_SET:
+ return canAddPatchSet(db());
+ case EDIT_ASSIGNEE:
+ return canEditAssignee();
+ case EDIT_DESCRIPTION:
+ return canEditDescription();
+ case EDIT_HASHTAGS:
+ return canEditHashtags();
+ case EDIT_TOPIC_NAME:
+ return canEditTopicName();
+ case REBASE:
+ return canRebase(db());
+ case RESTORE:
+ return canRestore(db());
+ case SUBMIT:
+ return getRefControl().canSubmit(isOwner());
+
+ case REMOVE_REVIEWER: // TODO Honor specific removal filters?
+ case SUBMIT_AS:
+ return getRefControl().canPerform(perm.permissionName().get());
+ }
+ } catch (OrmException e) {
+ throw new PermissionBackendException("unavailable", e);
+ }
+ throw new PermissionBackendException(perm + " unsupported");
+ }
+
+ private boolean can(LabelPermission perm) {
+ return !label(perm.permissionName().get()).isEmpty();
+ }
+
+ private boolean can(LabelPermission.WithValue perm) {
+ return label(perm.permissionName().get()).contains(perm.value());
+ }
+
+ private PermissionRange label(String permission) {
+ if (labels == null) {
+ labels = Maps.newHashMapWithExpectedSize(4);
+ }
+ PermissionRange r = labels.get(permission);
+ if (r == null) {
+ r = getRange(permission);
+ labels.put(permission, r);
+ }
+ return r;
+ }
+ }
+
+ static <T extends ChangePermissionOrLabel> Set<T> newSet(Collection<T> permSet) {
+ if (permSet instanceof EnumSet) {
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ Set<T> s = ((EnumSet) permSet).clone();
+ s.clear();
+ return s;
+ }
+ return Sets.newHashSetWithExpectedSize(permSet.size());
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
index 2f02728..0dcb5f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
@@ -58,6 +58,7 @@
InheritedBooleanInfo enableSignedPush = new InheritedBooleanInfo();
InheritedBooleanInfo requireSignedPush = new InheritedBooleanInfo();
InheritedBooleanInfo rejectImplicitMerges = new InheritedBooleanInfo();
+ InheritedBooleanInfo enableReviewerByEmail = new InheritedBooleanInfo();
useContributorAgreements.value = projectState.isUseContributorAgreements();
useSignedOffBy.value = projectState.isUseSignedOffBy();
@@ -73,6 +74,7 @@
enableSignedPush.configuredValue = p.getEnableSignedPush();
requireSignedPush.configuredValue = p.getRequireSignedPush();
rejectImplicitMerges.configuredValue = p.getRejectImplicitMerges();
+ enableReviewerByEmail.configuredValue = p.getEnableReviewerByEmail();
ProjectState parentState = Iterables.getFirst(projectState.parents(), null);
if (parentState != null) {
@@ -85,6 +87,7 @@
enableSignedPush.inheritedValue = projectState.isEnableSignedPush();
requireSignedPush.inheritedValue = projectState.isRequireSignedPush();
rejectImplicitMerges.inheritedValue = projectState.isRejectImplicitMerges();
+ enableReviewerByEmail.inheritedValue = projectState.isEnableReviewerByEmail();
}
this.useContributorAgreements = useContributorAgreements;
@@ -93,6 +96,7 @@
this.requireChangeId = requireChangeId;
this.rejectImplicitMerges = rejectImplicitMerges;
this.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
+ this.enableReviewerByEmail = enableReviewerByEmail;
if (serverEnableSignedPush) {
this.enableSignedPush = enableSignedPush;
this.requireSignedPush = requireSignedPush;
@@ -127,6 +131,8 @@
actions.put(d.getId(), new ActionInfo(d));
}
this.theme = projectState.getTheme();
+
+ this.extensionPanelNames = projectState.getConfig().getExtensionPanelSections();
}
private Map<String, Map<String, ConfigParameterInfo>> getPluginConfig(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
new file mode 100644
index 0000000..51fe493
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.FailedPermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+
+@Singleton
+class DefaultPermissionBackend extends PermissionBackend {
+ private final ProjectCache projectCache;
+
+ @Inject
+ DefaultPermissionBackend(ProjectCache projectCache) {
+ this.projectCache = projectCache;
+ }
+
+ @Override
+ public WithUser user(CurrentUser user) {
+ return new WithUserImpl(checkNotNull(user, "user"));
+ }
+
+ class WithUserImpl extends WithUser {
+ private final CurrentUser user;
+
+ WithUserImpl(CurrentUser user) {
+ this.user = checkNotNull(user, "user");
+ }
+
+ @Override
+ public ForProject project(Project.NameKey project) {
+ try {
+ ProjectState state = projectCache.checkedGet(project);
+ if (state != null) {
+ return state.controlFor(user).asForProject().database(db);
+ }
+ return FailedPermissionBackend.project("not found");
+ } catch (IOException e) {
+ return FailedPermissionBackend.project("unavailable", e);
+ }
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java
new file mode 100644
index 0000000..7a30863
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.inject.Scopes;
+
+/** Binds the default {@link PermissionBackend}. */
+public class DefaultPermissionBackendModule extends FactoryModule {
+ @Override
+ protected void configure() {
+ bind(PermissionBackend.class).to(DefaultPermissionBackend.class).in(Scopes.SINGLETON);
+
+ // TODO(sop) Hide ProjectControl, RefControl, ChangeControl related bindings.
+ bind(ProjectControl.GenericFactory.class);
+ factory(ProjectControl.AssistedFactory.class);
+ bind(ChangeControl.GenericFactory.class);
+ bind(ChangeControl.Factory.class);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java
index 1fadef6..373f561 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java
@@ -26,9 +26,9 @@
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -64,7 +64,7 @@
DeleteRef create(ProjectResource r);
}
- @AssistedInject
+ @Inject
DeleteRef(
Provider<IdentifiedUser> identifiedUser,
GitRepositoryManager repoManager,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
index d7af195..11f3805 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -24,6 +24,7 @@
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.server.change.CherryPickCommit;
public class Module extends RestApiModule {
@Override
@@ -96,6 +97,8 @@
get(PROJECT_KIND, "config").to(GetConfig.class);
put(PROJECT_KIND, "config").to(PutConfig.class);
+ post(COMMIT_KIND, "cherrypick").to(CherryPickCommit.class);
+
factory(DeleteRef.Factory.class);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
index 5e0ba28..16a3b6b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
@@ -46,7 +46,7 @@
public void start() {
int cpus = Runtime.getRuntime().availableProcessors();
if (config.getBoolean("cache", "projects", "loadOnStartup", false)) {
- final ThreadPoolExecutor pool =
+ ThreadPoolExecutor pool =
new ScheduledThreadPoolExecutor(
config.getInt("cache", "projects", "loadThreads", cpus),
new ThreadFactoryBuilder().setNameFormat("ProjectCacheLoader-%d").build());
@@ -54,25 +54,19 @@
log.info("Loading project cache");
scheduler.execute(
- new Runnable() {
- @Override
- public void run() {
- for (final Project.NameKey name : cache.all()) {
- pool.execute(
- new Runnable() {
- @Override
- public void run() {
- cache.get(name);
- }
- });
- }
- pool.shutdown();
- try {
- pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
- log.info("Finished loading project cache");
- } catch (InterruptedException e) {
- log.warn("Interrupted while waiting for project cache to load");
- }
+ () -> {
+ for (final Project.NameKey name : cache.all()) {
+ pool.execute(
+ () -> {
+ cache.get(name);
+ });
+ }
+ pool.shutdown();
+ try {
+ pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+ log.info("Finished loading project cache");
+ } catch (InterruptedException e) {
+ log.warn("Interrupted while waiting for project cache to load");
}
});
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index e9976c5..b188ee4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -25,6 +25,7 @@
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.metrics.Counter0;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.MetricMaker;
@@ -45,6 +46,10 @@
import com.google.gerrit.server.git.VisibleRefFilter;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.PermissionBackend.ForProject;
+import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
@@ -56,6 +61,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -565,4 +571,47 @@
Map<String, Ref> refs = filter.filter(m, true);
return !refs.isEmpty() && IncludedInResolver.includedInOne(repo, rw, commit, refs.values());
}
+
+ ForProject asForProject() {
+ return new ForProjectImpl();
+ }
+
+ private class ForProjectImpl extends ForProject {
+ @Override
+ public ForProject user(CurrentUser user) {
+ return forUser(user).asForProject().database(db);
+ }
+
+ @Override
+ public ForRef ref(String ref) {
+ return controlForRef(ref).asForRef().database(db);
+ }
+
+ @Override
+ public void check(ProjectPermission perm) throws AuthException, PermissionBackendException {
+ if (!can(perm)) {
+ throw new AuthException(perm.describeForException() + " not permitted");
+ }
+ }
+
+ @Override
+ public Set<ProjectPermission> test(Collection<ProjectPermission> permSet)
+ throws PermissionBackendException {
+ EnumSet<ProjectPermission> ok = EnumSet.noneOf(ProjectPermission.class);
+ for (ProjectPermission perm : permSet) {
+ if (can(perm)) {
+ ok.add(perm);
+ }
+ }
+ return ok;
+ }
+
+ private boolean can(ProjectPermission perm) throws PermissionBackendException {
+ switch (perm) {
+ case READ:
+ return isReadable();
+ }
+ throw new PermissionBackendException(perm + " unsupported");
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 8b8745e..32dc41f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -394,6 +394,10 @@
return getInheritableBoolean(Project::getRejectImplicitMerges);
}
+ public boolean isEnableReviewerByEmail() {
+ return getInheritableBoolean(Project::getEnableReviewerByEmail);
+ }
+
public LabelTypes getLabelTypes() {
Map<String, LabelType> types = new LinkedHashMap<>();
for (ProjectState s : treeInOrder()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index c5ded54..8c382b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -154,6 +154,10 @@
p.setState(input.state);
}
+ if (input.enableReviewerByEmail != null) {
+ p.setEnableReviewerByEmail(input.enableReviewerByEmail);
+ }
+
if (input.pluginConfigValues != null) {
setPluginConfigValues(ctrl.getProjectState(), projectConfig, input.pluginConfigValues);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 8413b5a9..50c024e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -14,17 +14,31 @@
package com.google.gerrit.server.project;
+import static com.google.common.base.Preconditions.checkArgument;
+
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.extensions.client.ProjectState;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.FailedPermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
+import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
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.HashSet;
import java.util.List;
@@ -173,11 +187,6 @@
return canPerform(Permission.SUBMIT, isChangeOwner) && canWrite();
}
- /** @return true if this user was granted submitAs to this ref */
- public boolean canSubmitAs() {
- return canPerform(Permission.SUBMIT_AS);
- }
-
/** @return true if the user can update the reference as a fast-forward. */
public boolean canUpdate() {
if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) {
@@ -393,7 +402,7 @@
}
/** @return true if this user can abandon a change for this ref */
- public boolean canAbandon() {
+ boolean canAbandon() {
return canPerform(Permission.ABANDON);
}
@@ -407,6 +416,11 @@
return canPerform(Permission.VIEW_DRAFTS);
}
+ /** @return true if this user can view private changes. */
+ public boolean canViewPrivateChanges() {
+ return canPerform(Permission.VIEW_PRIVATE_CHANGES);
+ }
+
/** @return true if this user can publish draft changes. */
public boolean canPublishDrafts() {
return canPerform(Permission.PUBLISH_DRAFTS);
@@ -423,16 +437,16 @@
}
/** @return true if this user can edit topic names. */
- public boolean canEditTopicName() {
+ boolean canEditTopicName() {
return canPerform(Permission.EDIT_TOPIC_NAME);
}
/** @return true if this user can edit hashtag names. */
- public boolean canEditHashtags() {
+ boolean canEditHashtags() {
return canPerform(Permission.EDIT_HASHTAGS);
}
- public boolean canEditAssignee() {
+ boolean canEditAssignee() {
return canPerform(Permission.EDIT_ASSIGNEE);
}
@@ -642,4 +656,77 @@
effective.put(permissionName, mine);
return mine;
}
+
+ ForRef asForRef() {
+ return new ForRefImpl();
+ }
+
+ private class ForRefImpl extends ForRef {
+ @Override
+ public ForRef user(CurrentUser user) {
+ return forUser(user).asForRef().database(db);
+ }
+
+ @Override
+ public ForChange change(ChangeData cd) {
+ try {
+ return cd.changeControl().forUser(getUser()).asForChange(cd, db);
+ } catch (OrmException e) {
+ return FailedPermissionBackend.change("unavailable", e);
+ }
+ }
+
+ @Override
+ public ForChange change(ChangeNotes notes) {
+ Change change = notes.getChange();
+ checkArgument(
+ getProjectControl().getProject().getNameKey().equals(change.getProject()),
+ "mismatched project");
+ return getProjectControl().controlFor(notes).asForChange(null, db);
+ }
+
+ @Override
+ public void check(RefPermission perm) throws AuthException, PermissionBackendException {
+ if (!can(perm)) {
+ throw new AuthException(perm.describeForException() + " not permitted");
+ }
+ }
+
+ @Override
+ public Set<RefPermission> test(Collection<RefPermission> permSet)
+ throws PermissionBackendException {
+ EnumSet<RefPermission> ok = EnumSet.noneOf(RefPermission.class);
+ for (RefPermission perm : permSet) {
+ if (can(perm)) {
+ ok.add(perm);
+ }
+ }
+ return ok;
+ }
+
+ private boolean can(RefPermission perm) throws PermissionBackendException {
+ switch (perm) {
+ case READ:
+ return isVisible();
+ case CREATE:
+ // TODO This isn't an accurate test.
+ return canPerform(perm.permissionName().get());
+ case DELETE:
+ return canDelete();
+ case UPDATE:
+ return canUpdate();
+ case FORCE_UPDATE:
+ return canForceUpdate();
+ case FORGE_AUTHOR:
+ return canForgeAuthor();
+ case FORGE_COMMITTER:
+ return canForgeCommitter();
+ case FORGE_SERVER:
+ return canForgeGerritServerIdentity();
+ case CREATE_CHANGE:
+ return canUpload();
+ }
+ throw new PermissionBackendException(perm + " unsupported");
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index c2b92aa..2c42502 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -18,7 +18,7 @@
import com.google.common.collect.Lists;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.index.IndexConfig;
import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gerrit.server.query.InternalQuery;
@@ -68,10 +68,6 @@
return query(AccountPredicates.defaultPredicate(query));
}
- public List<AccountState> byEmailPrefix(String emailPrefix) throws OrmException {
- return query(AccountPredicates.email(emailPrefix));
- }
-
public List<AccountState> byExternalId(String scheme, String id) throws OrmException {
return byExternalId(ExternalId.Key.create(scheme, id));
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BooleanPredicate.java
similarity index 83%
rename from gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/query/change/BooleanPredicate.java
index d998fa3..ad43c5f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BooleanPredicate.java
@@ -14,15 +14,15 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldDef.FillArgs;
-import com.google.gerrit.server.index.change.ChangeField;
import com.google.gwtorm.server.OrmException;
-class IsMergeablePredicate extends ChangeIndexPredicate {
+class BooleanPredicate extends ChangeIndexPredicate {
private final FillArgs args;
- IsMergeablePredicate(FillArgs args) {
- super(ChangeField.MERGEABLE, "1");
+ BooleanPredicate(FieldDef<ChangeData, String> field, FillArgs args) {
+ super(field, "1");
this.args = args;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index b62e3eb..1dbe5cd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -52,6 +52,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.StarredChangesUtil;
@@ -352,6 +353,7 @@
private StarsOf starsOf;
private ImmutableMap<Account.Id, StarRef> starRefs;
private ReviewerSet reviewers;
+ private ReviewerByEmailSet reviewersByEmail;
private List<ReviewerStatusUpdate> reviewerUpdates;
private PersonIdent author;
private PersonIdent committer;
@@ -954,6 +956,24 @@
return reviewers;
}
+ public ReviewerByEmailSet reviewersByEmail() throws OrmException {
+ if (reviewersByEmail == null) {
+ if (!lazyLoad) {
+ return ReviewerByEmailSet.empty();
+ }
+ reviewersByEmail = notes().getReviewersByEmail();
+ }
+ return reviewersByEmail;
+ }
+
+ public void setReviewersByEmail(ReviewerByEmailSet reviewersByEmail) {
+ this.reviewersByEmail = reviewersByEmail;
+ }
+
+ public ReviewerByEmailSet getReviewersByEmail() {
+ return reviewersByEmail;
+ }
+
public List<ReviewerStatusUpdate> reviewerUpdates() throws OrmException {
if (reviewerUpdates == null) {
if (!lazyLoad) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
index 0604f8b..0362c85 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
@@ -14,9 +14,12 @@
package com.google.gerrit.server.query.change;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.IndexPredicate;
import com.google.gerrit.server.query.Matchable;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
public abstract class ChangeIndexPredicate extends IndexPredicate<ChangeData>
implements Matchable<ChangeData> {
@@ -27,4 +30,11 @@
protected ChangeIndexPredicate(FieldDef<ChangeData, ?> def, String name, String value) {
super(def, name, value);
}
+
+ protected static Predicate<ChangeData> create(Arguments args, Predicate<ChangeData> p) {
+ if (!args.allowsDrafts) {
+ return Predicate.and(p, Predicate.not(new ChangeStatusPredicate(Change.Status.DRAFT)));
+ }
+ return p;
+ }
}
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 af2cb60..6b66c41 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
@@ -61,8 +61,10 @@
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.change.ChangeIndexCollection;
import com.google.gerrit.server.index.change.ChangeIndexRewriter;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ListChildProjects;
@@ -149,6 +151,7 @@
public static final String FIELD_OWNERIN = "ownerin";
public static final String FIELD_PARENTPROJECT = "parentproject";
public static final String FIELD_PATH = "path";
+ public static final String FIELD_PRIVATE = "private";
public static final String FIELD_PROJECT = "project";
public static final String FIELD_PROJECTS = "projects";
public static final String FIELD_REF = "ref";
@@ -569,7 +572,11 @@
}
if ("mergeable".equalsIgnoreCase(value)) {
- return new IsMergeablePredicate(args.fillArgs);
+ return new BooleanPredicate(ChangeField.MERGEABLE, args.fillArgs);
+ }
+
+ if ("private".equalsIgnoreCase(value)) {
+ return new BooleanPredicate(ChangeField.PRIVATE, args.fillArgs);
}
if ("assigned".equalsIgnoreCase(value)) {
@@ -937,17 +944,12 @@
@Operator
public Predicate<ChangeData> reviewer(String who) throws QueryParseException, OrmException {
- return Predicate.or(
- parseAccount(who)
- .stream()
- .map(id -> ReviewerPredicate.reviewer(args, id))
- .collect(toList()));
+ return reviewerByState(who, ReviewerStateInternal.REVIEWER);
}
@Operator
public Predicate<ChangeData> cc(String who) throws QueryParseException, OrmException {
- return Predicate.or(
- parseAccount(who).stream().map(id -> ReviewerPredicate.cc(args, id)).collect(toList()));
+ return reviewerByState(who, ReviewerStateInternal.CC);
}
@Operator
@@ -1176,4 +1178,37 @@
private Account.Id self() throws QueryParseException {
return args.getIdentifiedUser().getAccountId();
}
+
+ public Predicate<ChangeData> reviewerByState(String who, ReviewerStateInternal state)
+ throws QueryParseException, OrmException {
+ Predicate<ChangeData> reviewerByEmailPredicate = null;
+ if (args.index.getSchema().hasField(ChangeField.REVIEWER_BY_EMAIL)) {
+ Address address = Address.tryParse(who);
+ if (address != null) {
+ reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(args, address, state);
+ }
+ }
+
+ Predicate<ChangeData> reviewerPredicate = null;
+ try {
+ reviewerPredicate =
+ Predicate.or(
+ parseAccount(who)
+ .stream()
+ .map(id -> ReviewerPredicate.forState(args, id, state))
+ .collect(toList()));
+ } catch (QueryParseException e) {
+ // Propagate this exception only if we can't use 'who' to query by email
+ if (reviewerByEmailPredicate == null) {
+ throw e;
+ }
+ }
+
+ if (reviewerPredicate != null && reviewerByEmailPredicate != null) {
+ return Predicate.or(reviewerPredicate, reviewerByEmailPredicate);
+ } else if (reviewerPredicate != null) {
+ return reviewerPredicate;
+ }
+ return reviewerByEmailPredicate;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
new file mode 100644
index 0000000..a040e18
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.notedb.ReviewerStateInternal;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
+import com.google.gwtorm.server.OrmException;
+
+class ReviewerByEmailPredicate extends ChangeIndexPredicate {
+
+ static Predicate<ChangeData> forState(Arguments args, Address adr, ReviewerStateInternal state) {
+ checkArgument(state != ReviewerStateInternal.REMOVED, "can't query by removed reviewer");
+ return create(args, new ReviewerByEmailPredicate(state, adr));
+ }
+
+ private final ReviewerStateInternal state;
+ private final Address adr;
+
+ private ReviewerByEmailPredicate(ReviewerStateInternal state, Address adr) {
+ super(ChangeField.REVIEWER_BY_EMAIL, ChangeField.getReviewerByEmailFieldValue(state, adr));
+ this.state = state;
+ this.adr = adr;
+ }
+
+ Address getAddress() {
+ return adr;
+ }
+
+ @Override
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.reviewersByEmail().asTable().get(state, adr) != null;
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index 6ce02fb..5b86494 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -14,10 +14,10 @@
package com.google.gerrit.server.query.change;
+import static com.google.common.base.Preconditions.checkArgument;
import static java.util.stream.Collectors.toList;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.query.Predicate;
@@ -26,6 +26,12 @@
import java.util.stream.Stream;
class ReviewerPredicate extends ChangeIndexPredicate {
+ static Predicate<ChangeData> forState(
+ Arguments args, Account.Id id, ReviewerStateInternal state) {
+ checkArgument(state != ReviewerStateInternal.REMOVED, "can't query by removed reviewer");
+ return create(args, new ReviewerPredicate(state, id));
+ }
+
static Predicate<ChangeData> reviewer(Arguments args, Account.Id id) {
Predicate<ChangeData> p;
if (args.notesMigration.readChanges()) {
@@ -54,15 +60,6 @@
.collect(toList()));
}
- private static Predicate<ChangeData> create(Arguments args, Predicate<ChangeData> p) {
- if (!args.allowsDrafts) {
- // TODO(dborowitz): This really belongs much higher up e.g. QueryProcessor. Also, why are we
- // even doing this?
- return Predicate.and(p, Predicate.not(new ChangeStatusPredicate(Change.Status.DRAFT)));
- }
- return p;
- }
-
private final ReviewerStateInternal state;
private final Account.Id id;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 170a5fa..c5bd140 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -161,8 +161,8 @@
}
}
- private void exportPoolMetrics(final BasicDataSource pool) {
- final CallbackMetric1<Boolean, Integer> cnt =
+ private void exportPoolMetrics(BasicDataSource pool) {
+ CallbackMetric1<Boolean, Integer> cnt =
metrics.newCallbackMetric(
"sql/connection_pool/connections",
Integer.class,
@@ -170,13 +170,10 @@
Field.ofBoolean("active"));
metrics.newTrigger(
cnt,
- new Runnable() {
- @Override
- public void run() {
- synchronized (pool) {
- cnt.set(true, pool.getNumActive());
- cnt.set(false, pool.getNumIdle());
- }
+ () -> {
+ synchronized (pool) {
+ cnt.set(true, pool.getNumActive());
+ cnt.set(false, pool.getNumIdle());
}
});
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
index 43e9a3a..af55b00 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
@@ -73,6 +73,11 @@
}
@Override
+ public boolean changesTablesEnabled() {
+ return false;
+ }
+
+ @Override
public ChangeAccess changes() {
return changes;
}
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 a67a8a9..caeba3a 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
@@ -35,7 +35,7 @@
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
- public static final Class<Schema_142> C = Schema_142.class;
+ public static final Class<Schema_145> C = Schema_145.class;
public static int getBinaryVersion() {
return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_143.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_143.java
new file mode 100644
index 0000000..b190b29
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_143.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/** Add isPrivate field to change. */
+public class Schema_143 extends SchemaVersion {
+ @Inject
+ Schema_143(Provider<Schema_142> prior) {
+ super(prior);
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_144.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_144.java
new file mode 100644
index 0000000..70e55cf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_144.java
@@ -0,0 +1,78 @@
+// 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.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIdReader;
+import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.IOException;
+import java.sql.SQLException;
+import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+public class Schema_144 extends SchemaVersion {
+ private static final String COMMIT_MSG = "Import external IDs from ReviewDb";
+
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsersName;
+ private final PersonIdent serverIdent;
+
+ @Inject
+ Schema_144(
+ Provider<Schema_143> prior,
+ GitRepositoryManager repoManager,
+ AllUsersName allUsersName,
+ @GerritPersonIdent PersonIdent serverIdent) {
+ super(prior);
+ this.repoManager = repoManager;
+ this.allUsersName = allUsersName;
+ this.serverIdent = serverIdent;
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+ Set<ExternalId> toAdd = ExternalId.from(db.accountExternalIds().all().toList());
+ try {
+ try (Repository repo = repoManager.openRepository(allUsersName);
+ RevWalk rw = new RevWalk(repo);
+ ObjectInserter ins = repo.newObjectInserter()) {
+ ObjectId rev = ExternalIdReader.readRevision(repo);
+
+ NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
+
+ for (ExternalId extId : toAdd) {
+ ExternalIdsUpdate.upsert(rw, ins, noteMap, extId);
+ }
+
+ ExternalIdsUpdate.commit(repo, rw, ins, rev, noteMap, COMMIT_MSG, serverIdent, serverIdent);
+ }
+ } catch (IOException | ConfigInvalidException e) {
+ throw new OrmException("Failed to migrate external IDs to NoteDb", e);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_145.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_145.java
new file mode 100644
index 0000000..6ccb5d8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_145.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.sql.SQLException;
+
+/** Create account_external_ids_byEmail index. */
+public class Schema_145 extends SchemaVersion {
+
+ @Inject
+ Schema_145(Provider<Schema_144> prior) {
+ super(prior);
+ }
+
+ @Override
+ protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
+ JdbcSchema schema = (JdbcSchema) db;
+ SqlDialect dialect = schema.getDialect();
+ try (StatementExecutor e = newExecutor(db)) {
+ try {
+ dialect.dropIndex(e, "account_external_ids", "account_external_ids_byEmail");
+ } catch (OrmException ex) {
+ // Ignore. The index did not exist.
+ }
+ e.execute(
+ "CREATE INDEX account_external_ids_byEmail"
+ + " ON account_external_ids"
+ + " (email_address)");
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java
index f34c22cb..0b88ef0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -18,18 +18,27 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
+import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.config.FactoryModule;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.validators.OnSubmitValidators;
+import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.NoSuchRefException;
import com.google.gerrit.server.util.RequestId;
import com.google.inject.Inject;
import com.google.inject.Module;
@@ -41,6 +50,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.TimeZone;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.ObjectInserter;
@@ -81,24 +91,36 @@
@Override
public void configure() {
factory(ReviewDbBatchUpdate.AssistedFactory.class);
+ factory(NoteDbBatchUpdate.AssistedFactory.class);
}
};
}
@Singleton
public static class Factory {
+ private final NotesMigration migration;
private final ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory;
+ private final NoteDbBatchUpdate.AssistedFactory noteDbBatchUpdateFactory;
@Inject
- Factory(ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory) {
+ Factory(
+ NotesMigration migration,
+ ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory,
+ NoteDbBatchUpdate.AssistedFactory noteDbBatchUpdateFactory) {
+ this.migration = migration;
this.reviewDbBatchUpdateFactory = reviewDbBatchUpdateFactory;
+ this.noteDbBatchUpdateFactory = noteDbBatchUpdateFactory;
}
public BatchUpdate create(
ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when) {
+ if (migration.disableChangeReviewDb()) {
+ return noteDbBatchUpdateFactory.create(db, project, user, when);
+ }
return reviewDbBatchUpdateFactory.create(db, project, user, when);
}
+ @SuppressWarnings({"rawtypes", "unchecked"})
public void execute(
Collection<BatchUpdate> updates,
BatchUpdateListener listener,
@@ -110,14 +132,33 @@
// method above, which always returns instances of the type we expect. Just to be safe,
// copy them into an ImmutableList so there is no chance the callee can pollute the input
// collection.
- @SuppressWarnings({"rawtypes", "unchecked"})
- ImmutableList<ReviewDbBatchUpdate> reviewDbUpdates =
- (ImmutableList) ImmutableList.copyOf(updates);
- ReviewDbBatchUpdate.execute(reviewDbUpdates, listener, requestId, dryRun);
+ if (migration.disableChangeReviewDb()) {
+ ImmutableList<NoteDbBatchUpdate> noteDbUpdates =
+ (ImmutableList) ImmutableList.copyOf(updates);
+ NoteDbBatchUpdate.execute(noteDbUpdates, listener, requestId, dryRun);
+ } else {
+ ImmutableList<ReviewDbBatchUpdate> reviewDbUpdates =
+ (ImmutableList) ImmutableList.copyOf(updates);
+ ReviewDbBatchUpdate.execute(reviewDbUpdates, listener, requestId, dryRun);
+ }
}
}
- protected static Order getOrder(Collection<? extends BatchUpdate> updates) {
+ static void setRequestIds(
+ Collection<? extends BatchUpdate> updates, @Nullable RequestId requestId) {
+ if (requestId != null) {
+ for (BatchUpdate u : updates) {
+ checkArgument(
+ u.requestId == null || u.requestId == requestId,
+ "refusing to overwrite RequestId %s in update with %s",
+ u.requestId,
+ requestId);
+ u.setRequestId(requestId);
+ }
+ }
+ }
+
+ static Order getOrder(Collection<? extends BatchUpdate> updates) {
Order o = null;
for (BatchUpdate u : updates) {
if (o == null) {
@@ -129,7 +170,7 @@
return o;
}
- protected static boolean getUpdateChangesInParallel(Collection<? extends BatchUpdate> updates) {
+ static boolean getUpdateChangesInParallel(Collection<? extends BatchUpdate> updates) {
checkArgument(!updates.isEmpty());
Boolean p = null;
for (BatchUpdate u : updates) {
@@ -148,6 +189,28 @@
return p;
}
+ static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException {
+ Throwables.throwIfUnchecked(e);
+
+ // Propagate REST API exceptions thrown by operations; they commonly throw exceptions like
+ // ResourceConflictException to indicate an atomic update failure.
+ Throwables.throwIfInstanceOf(e, UpdateException.class);
+ Throwables.throwIfInstanceOf(e, RestApiException.class);
+
+ // Convert other common non-REST exception types with user-visible messages to corresponding
+ // REST exception types
+ if (e instanceof InvalidChangeOperationException) {
+ throw new ResourceConflictException(e.getMessage(), e);
+ } else if (e instanceof NoSuchChangeException
+ || e instanceof NoSuchRefException
+ || e instanceof NoSuchProjectException) {
+ throw new ResourceNotFoundException(e.getMessage(), e);
+ }
+
+ // Otherwise, wrap in a generic UpdateException, which does not include a user-visible message.
+ throw new UpdateException(e);
+ }
+
protected GitRepositoryManager repoManager;
protected final Project.NameKey project;
@@ -199,7 +262,9 @@
public abstract void execute(BatchUpdateListener listener)
throws UpdateException, RestApiException;
- public abstract void execute() throws UpdateException, RestApiException;
+ public void execute() throws UpdateException, RestApiException {
+ execute(BatchUpdateListener.NONE);
+ }
protected abstract Context newContext();
@@ -232,7 +297,13 @@
return this;
}
- /** Execute {@link BatchUpdateOp#updateChange(ChangeContext)} in parallel for each change. */
+ /**
+ * Execute {@link BatchUpdateOp#updateChange(ChangeContext)} in parallel for each change.
+ *
+ * <p>This improves performance of writing to multiple changes in separate ReviewDb transactions.
+ * When only NoteDb is used, updates to all changes are written in a single batch ref update, so
+ * parallelization is not used and this option is ignored.
+ */
public BatchUpdate updateChangesInParallel() {
this.updateChangesInParallel = true;
return this;
@@ -252,6 +323,12 @@
return user;
}
+ protected Optional<Account> getAccount() {
+ return user.isIdentifiedUser()
+ ? Optional.of(user.asIdentifiedUser().getAccount())
+ : Optional.empty();
+ }
+
protected Repository getRepository() throws IOException {
initRepository();
return repo;
@@ -284,7 +361,7 @@
return this;
}
- public BatchUpdate insertChange(InsertChangeOp op) {
+ public BatchUpdate insertChange(InsertChangeOp op) throws IOException {
Context ctx = newContext();
Change c = op.createChange(ctx);
checkArgument(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/ChangeContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/ChangeContext.java
index d619490..ca39763 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/ChangeContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/ChangeContext.java
@@ -44,17 +44,24 @@
ChangeUpdate getUpdate(PatchSet.Id psId);
/**
- * @return control for this change. The user will be the same as {@link #getUser()}, and the
- * change data is read within the same transaction that {@code updateChange} is executing.
+ * Get the control for this change, encapsulating the user and up-to-date change data.
+ *
+ * <p>The user will be the same as {@link #getUser()}, and the change data is read within the same
+ * transaction that {@link BatchUpdateOp#updateChange(ChangeContext)} is executing.
+ *
+ * @return control for this change.
*/
ChangeControl getControl();
/**
- * @param bump whether to bump the value of {@link Change#getLastUpdatedOn()} field before storing
- * to ReviewDb. For NoteDb, the value is always incremented (assuming the update is not
- * otherwise a no-op).
+ * Don't bump the value of {@link Change#getLastUpdatedOn()}.
+ *
+ * <p>If called, don't bump the timestamp before storing to ReviewDb. Only has an effect in
+ * ReviewDb, and the only usage should be to match the behavior of NoteDb. Specifically, in NoteDb
+ * the timestamp is updated if and only if the change meta graph is updated, and is not updated
+ * when only drafts are modified.
*/
- void bumpLastUpdatedOn(boolean bump);
+ void dontBumpLastUpdatedOn();
/**
* Instruct {@link BatchUpdate} to delete this change.
@@ -63,7 +70,11 @@
*/
void deleteChange();
- /** @return notes corresponding to {@link #getControl()}. */
+ /**
+ * Get notes corresponding to {@link #getControl()}.
+ *
+ * @return loaded notes instance.
+ */
default ChangeNotes getNotes() {
return checkNotNull(getControl().getNotes());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/Context.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/Context.java
index 497b7ab..db9239f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/Context.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/Context.java
@@ -33,7 +33,11 @@
* <p>A single update may span multiple changes, but they all belong to a single repo.
*/
public interface Context {
- /** @return the project name this update operates on. */
+ /**
+ * Get the project name this update operates on.
+ *
+ * @return project.
+ */
Project.NameKey getProject();
/**
@@ -57,50 +61,80 @@
*/
RevWalk getRevWalk() throws IOException;
- /** @return the timestamp at which this update takes place. */
+ /**
+ * Get the timestamp at which this update takes place.
+ *
+ * @return timestamp.
+ */
Timestamp getWhen();
/**
- * @return the time zone in which this update takes place. In the current implementation, this is
- * always the time zone of the server.
+ * Get the time zone in which this update takes place.
+ *
+ * <p>In the current implementation, this is always the time zone of the server.
+ *
+ * @return time zone.
*/
TimeZone getTimeZone();
/**
- * @return an open ReviewDb database. Callers should not manage transactions or call mutating
- * methods on the Changes table. Mutations on other tables (including other entities in the
- * change entity group) are fine.
+ * Get the ReviewDb database.
+ *
+ * <p>Callers should not manage transactions or call mutating methods on the Changes table.
+ * Mutations on other tables (including other entities in the change entity group) are fine.
+ *
+ * @return open database instance.
*/
ReviewDb getDb();
/**
- * @return user performing the update. In the current implementation, this is always an {@link
- * IdentifiedUser} or {@link com.google.gerrit.server.InternalUser}.
+ * Get the user performing the update.
+ *
+ * <p>In the current implementation, this is always an {@link IdentifiedUser} or {@link
+ * com.google.gerrit.server.InternalUser}.
+ *
+ * @return user.
*/
CurrentUser getUser();
- /** @return order in which operations are executed in this update. */
+ /**
+ * Get the order in which operations are executed in this update.
+ *
+ * @return order of operations.
+ */
Order getOrder();
/**
- * @return identified user performing the update; throws an unchecked exception if the user is not
- * an {@link IdentifiedUser}
+ * Get the identified user performing the update.
+ *
+ * <p>Convenience method for {@code getUser().asIdentifiedUser()}.
+ *
+ * @see CurrentUser#asIdentifiedUser()
+ * @return user.
*/
default IdentifiedUser getIdentifiedUser() {
return checkNotNull(getUser()).asIdentifiedUser();
}
/**
- * @return account of the user performing the update; throws if the user is not an {@link
- * IdentifiedUser}
+ * Get the account of the user performing the update.
+ *
+ * <p>Convenience method for {@code getIdentifiedUser().getAccount()}.
+ *
+ * @see CurrentUser#asIdentifiedUser()
+ * @return account.
*/
default Account getAccount() {
return getIdentifiedUser().getAccount();
}
/**
- * @return account ID of the user performing the update; throws if the user is not an {@link
- * IdentifiedUser}
+ * Get the account ID of the user performing the update.
+ *
+ * <p>Convenience method for {@code getUser().getAccountId()}
+ *
+ * @see CurrentUser#getAccountId()
+ * @return account ID.
*/
default Account.Id getAccountId() {
return getIdentifiedUser().getAccountId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/InsertChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/InsertChangeOp.java
index 1a947e6..7060059 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/InsertChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/InsertChangeOp.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.update;
import com.google.gerrit.reviewdb.client.Change;
+import java.io.IOException;
/**
* Specialization of {@link BatchUpdateOp} for creating changes.
@@ -27,5 +28,5 @@
* first.
*/
public interface InsertChangeOp extends BatchUpdateOp {
- Change createChange(Context ctx);
+ Change createChange(Context ctx) throws IOException;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
new file mode 100644
index 0000000..41e37ef
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
@@ -0,0 +1,470 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.update;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.Comparator.comparing;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.index.change.ChangeIndexer;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NoteDbUpdateManager;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.util.RequestId;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.TreeMap;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+/**
+ * {@link BatchUpdate} implementation that only supports NoteDb.
+ *
+ * <p>Used when {@code noteDb.changes.disableReviewDb=true}, at which point ReviewDb is not
+ * consulted during updates.
+ */
+class NoteDbBatchUpdate extends BatchUpdate {
+ interface AssistedFactory {
+ NoteDbBatchUpdate create(
+ ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when);
+ }
+
+ static void execute(
+ ImmutableList<NoteDbBatchUpdate> updates,
+ BatchUpdateListener listener,
+ @Nullable RequestId requestId,
+ boolean dryrun)
+ throws UpdateException, RestApiException {
+ if (updates.isEmpty()) {
+ return;
+ }
+ setRequestIds(updates, requestId);
+
+ try {
+ Order order = getOrder(updates);
+ // TODO(dborowitz): Fuse implementations to use a single BatchRefUpdate between phases. Note
+ // that we will still need to respect the order, since it also dictates the order in which
+ // listener methods are called. We can revisit this later, particularly since the only user of
+ // BatchUpdateListener is MergeOp, which only uses one order.
+ switch (order) {
+ case REPO_BEFORE_DB:
+ for (NoteDbBatchUpdate u : updates) {
+ u.executeUpdateRepo();
+ }
+ listener.afterUpdateRepos();
+ for (NoteDbBatchUpdate u : updates) {
+ u.executeRefUpdates(dryrun);
+ }
+ listener.afterUpdateRefs();
+ for (NoteDbBatchUpdate u : updates) {
+ u.reindexChanges(u.executeChangeOps(dryrun), dryrun);
+ }
+ listener.afterUpdateChanges();
+ break;
+ case DB_BEFORE_REPO:
+ for (NoteDbBatchUpdate u : updates) {
+ u.reindexChanges(u.executeChangeOps(dryrun), dryrun);
+ }
+ listener.afterUpdateChanges();
+ for (NoteDbBatchUpdate u : updates) {
+ u.executeUpdateRepo();
+ }
+ listener.afterUpdateRepos();
+ for (NoteDbBatchUpdate u : updates) {
+ u.executeRefUpdates(dryrun);
+ }
+ listener.afterUpdateRefs();
+ break;
+ default:
+ throw new IllegalStateException("invalid execution order: " + order);
+ }
+
+ ChangeIndexer.allAsList(
+ updates.stream().flatMap(u -> u.indexFutures.stream()).collect(toList()))
+ .get();
+
+ // Fire ref update events only after all mutations are finished, since callers may assume a
+ // patch set ref being created means the change was created, or a branch advancing meaning
+ // some changes were closed.
+ updates
+ .stream()
+ .filter(u -> u.batchRefUpdate != null)
+ .forEach(
+ u -> u.gitRefUpdated.fire(u.project, u.batchRefUpdate, u.getAccount().orElse(null)));
+
+ if (!dryrun) {
+ for (NoteDbBatchUpdate u : updates) {
+ u.executePostOps();
+ }
+ }
+ } catch (Exception e) {
+ wrapAndThrowException(e);
+ }
+ }
+
+ class ContextImpl implements Context {
+ private Repository repoWrapper;
+
+ @Override
+ public Repository getRepository() throws IOException {
+ if (repoWrapper == null) {
+ repoWrapper = new ReadOnlyRepository(NoteDbBatchUpdate.this.getRepository());
+ }
+ return repoWrapper;
+ }
+
+ @Override
+ public RevWalk getRevWalk() throws IOException {
+ return NoteDbBatchUpdate.this.getRevWalk();
+ }
+
+ @Override
+ public Project.NameKey getProject() {
+ return project;
+ }
+
+ @Override
+ public Timestamp getWhen() {
+ return when;
+ }
+
+ @Override
+ public TimeZone getTimeZone() {
+ return tz;
+ }
+
+ @Override
+ public ReviewDb getDb() {
+ return db;
+ }
+
+ @Override
+ public CurrentUser getUser() {
+ return user;
+ }
+
+ @Override
+ public Order getOrder() {
+ return order;
+ }
+ }
+
+ private class RepoContextImpl extends ContextImpl implements RepoContext {
+ @Override
+ public Repository getRepository() throws IOException {
+ return NoteDbBatchUpdate.this.getRepository();
+ }
+
+ @Override
+ public ObjectInserter getInserter() throws IOException {
+ return NoteDbBatchUpdate.this.getObjectInserter();
+ }
+
+ @Override
+ public void addRefUpdate(ReceiveCommand cmd) throws IOException {
+ initRepository();
+ commands.add(cmd);
+ }
+ }
+
+ private class ChangeContextImpl extends ContextImpl implements ChangeContext {
+ private final ChangeControl ctl;
+ private final Map<PatchSet.Id, ChangeUpdate> updates;
+
+ private boolean deleted;
+
+ protected ChangeContextImpl(ChangeControl ctl) {
+ this.ctl = checkNotNull(ctl);
+ updates = new TreeMap<>(comparing(PatchSet.Id::get));
+ }
+
+ @Override
+ public ChangeUpdate getUpdate(PatchSet.Id psId) {
+ ChangeUpdate u = updates.get(psId);
+ if (u == null) {
+ u = changeUpdateFactory.create(ctl, when);
+ if (newChanges.containsKey(ctl.getId())) {
+ u.setAllowWriteToNewRef(true);
+ }
+ u.setPatchSetId(psId);
+ updates.put(psId, u);
+ }
+ return u;
+ }
+
+ @Override
+ public ChangeControl getControl() {
+ return ctl;
+ }
+
+ @Override
+ public void dontBumpLastUpdatedOn() {
+ // Do nothing; NoteDb effectively updates timestamp if and only if a commit was written to the
+ // change meta ref.
+ }
+
+ @Override
+ public void deleteChange() {
+ deleted = true;
+ }
+ }
+
+ /** Per-change result status from {@link #executeChangeOps}. */
+ private enum ChangeResult {
+ SKIPPED,
+ UPSERTED,
+ DELETED;
+ }
+
+ private final ChangeNotes.Factory changeNotesFactory;
+ private final ChangeControl.GenericFactory changeControlFactory;
+ private final ChangeUpdate.Factory changeUpdateFactory;
+ private final NoteDbUpdateManager.Factory updateManagerFactory;
+ private final ChangeIndexer indexer;
+ private final GitReferenceUpdated gitRefUpdated;
+ private final ReviewDb db;
+
+ private List<CheckedFuture<?, IOException>> indexFutures;
+
+ @Inject
+ NoteDbBatchUpdate(
+ GitRepositoryManager repoManager,
+ @GerritPersonIdent PersonIdent serverIdent,
+ ChangeNotes.Factory changeNotesFactory,
+ ChangeControl.GenericFactory changeControlFactory,
+ ChangeUpdate.Factory changeUpdateFactory,
+ NoteDbUpdateManager.Factory updateManagerFactory,
+ ChangeIndexer indexer,
+ GitReferenceUpdated gitRefUpdated,
+ @Assisted ReviewDb db,
+ @Assisted Project.NameKey project,
+ @Assisted CurrentUser user,
+ @Assisted Timestamp when) {
+ super(repoManager, serverIdent, project, user, when);
+ checkArgument(!db.changesTablesEnabled(), "expected Change tables to be disabled on %s", db);
+ this.changeNotesFactory = changeNotesFactory;
+ this.changeControlFactory = changeControlFactory;
+ this.changeUpdateFactory = changeUpdateFactory;
+ this.updateManagerFactory = updateManagerFactory;
+ this.indexer = indexer;
+ this.gitRefUpdated = gitRefUpdated;
+ this.db = db;
+ this.indexFutures = new ArrayList<>();
+ }
+
+ @Override
+ public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException {
+ execute(ImmutableList.of(this), listener, requestId, false);
+ }
+
+ @Override
+ protected Context newContext() {
+ return new ContextImpl();
+ }
+
+ private void executeUpdateRepo() throws UpdateException, RestApiException {
+ try {
+ logDebug("Executing updateRepo on {} ops", ops.size());
+ RepoContextImpl ctx = new RepoContextImpl();
+ for (BatchUpdateOp op : ops.values()) {
+ op.updateRepo(ctx);
+ }
+
+ logDebug("Executing updateRepo on {} RepoOnlyOps", repoOnlyOps.size());
+ for (RepoOnlyOp op : repoOnlyOps) {
+ op.updateRepo(ctx);
+ }
+
+ if (onSubmitValidators != null && commands != null && !commands.isEmpty()) {
+ // Validation of refs has to take place here and not at the beginning
+ // executeRefUpdates. Otherwise failing validation in a second
+ // BatchUpdate object will happen *after* first object's
+ // executeRefUpdates has finished, hence after first repo's refs have
+ // been updated, which is too late.
+ onSubmitValidators.validate(
+ project,
+ new ReadOnlyRepository(getRepository()),
+ ctx.getInserter().newReader(),
+ commands.getCommands());
+ }
+
+ // TODO(dborowitz): Don't flush when fusing phases.
+ if (inserter != null) {
+ logDebug("Flushing inserter");
+ inserter.flush();
+ } else {
+ logDebug("No objects to flush");
+ }
+ } catch (Exception e) {
+ Throwables.throwIfInstanceOf(e, RestApiException.class);
+ throw new UpdateException(e);
+ }
+ }
+
+ // TODO(dborowitz): Don't execute non-change ref updates separately when fusing phases.
+ private void executeRefUpdates(boolean dryrun) throws IOException, RestApiException {
+ if (commands == null || commands.isEmpty()) {
+ logDebug("No ref updates to execute");
+ return;
+ }
+ // May not be opened if the caller added ref updates but no new objects.
+ initRepository();
+ batchRefUpdate = repo.getRefDatabase().newBatchUpdate();
+ commands.addTo(batchRefUpdate);
+ logDebug("Executing batch of {} ref updates", batchRefUpdate.getCommands().size());
+ if (dryrun) {
+ return;
+ }
+
+ batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE);
+ boolean ok = true;
+ for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
+ if (cmd.getResult() != ReceiveCommand.Result.OK) {
+ ok = false;
+ break;
+ }
+ }
+ if (!ok) {
+ throw new RestApiException("BatchRefUpdate failed: " + batchRefUpdate);
+ }
+ }
+
+ private Map<Change.Id, ChangeResult> executeChangeOps(boolean dryrun) throws Exception {
+ logDebug("Executing change ops");
+ Map<Change.Id, ChangeResult> result =
+ Maps.newLinkedHashMapWithExpectedSize(ops.keySet().size());
+ Repository repo = getRepository();
+ // TODO(dborowitz): Teach NoteDbUpdateManager to allow reusing the same inserter and batch ref
+ // update as in executeUpdateRepo.
+ try (ObjectInserter ins = repo.newObjectInserter();
+ RevWalk rw = new RevWalk(ins.newReader());
+ NoteDbUpdateManager updateManager =
+ updateManagerFactory
+ .create(project)
+ .setChangeRepo(repo, rw, ins, new ChainedReceiveCommands(repo))) {
+ if (user.isIdentifiedUser()) {
+ updateManager.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, tz));
+ }
+ for (Map.Entry<Change.Id, Collection<BatchUpdateOp>> e : ops.asMap().entrySet()) {
+ Change.Id id = e.getKey();
+ ChangeContextImpl ctx = newChangeContext(id);
+ boolean dirty = false;
+ logDebug("Applying {} ops for change {}", e.getValue().size(), id);
+ for (BatchUpdateOp op : e.getValue()) {
+ dirty |= op.updateChange(ctx);
+ }
+ if (!dirty) {
+ logDebug("No ops reported dirty, short-circuiting");
+ result.put(id, ChangeResult.SKIPPED);
+ continue;
+ }
+ for (ChangeUpdate u : ctx.updates.values()) {
+ updateManager.add(u);
+ }
+ if (ctx.deleted) {
+ logDebug("Change {} was deleted", id);
+ updateManager.deleteChange(id);
+ result.put(id, ChangeResult.DELETED);
+ } else {
+ result.put(id, ChangeResult.UPSERTED);
+ }
+ }
+
+ if (!dryrun) {
+ logDebug("Executing NoteDb updates");
+ updateManager.execute();
+ }
+ }
+ return result;
+ }
+
+ private ChangeContextImpl newChangeContext(Change.Id id) throws OrmException {
+ logDebug("Opening change {} for update", id);
+ Change c = newChanges.get(id);
+ boolean isNew = c != null;
+ if (!isNew) {
+ // Pass a synthetic change into ChangeNotes.Factory, which will take care of checking for
+ // existence and populating columns from the parsed notes state.
+ // TODO(dborowitz): This dance made more sense when using Reviewdb; consider a nicer way.
+ c = ChangeNotes.Factory.newNoteDbOnlyChange(project, id);
+ } else {
+ logDebug("Change {} is new", id);
+ }
+ ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew);
+ ChangeControl ctl = changeControlFactory.controlFor(notes, user);
+ return new ChangeContextImpl(ctl);
+ }
+
+ private void reindexChanges(Map<Change.Id, ChangeResult> updateResults, boolean dryrun) {
+ if (dryrun) {
+ return;
+ }
+ logDebug("Reindexing {} changes", updateResults.size());
+ for (Map.Entry<Change.Id, ChangeResult> e : updateResults.entrySet()) {
+ Change.Id id = e.getKey();
+ switch (e.getValue()) {
+ case UPSERTED:
+ indexFutures.add(indexer.indexAsync(project, id));
+ break;
+ case DELETED:
+ indexFutures.add(indexer.deleteAsync(id));
+ break;
+ case SKIPPED:
+ break;
+ default:
+ throw new IllegalStateException("unexpected result: " + e.getValue());
+ }
+ }
+ }
+
+ private void executePostOps() throws Exception {
+ ContextImpl ctx = new ContextImpl();
+ for (BatchUpdateOp op : ops.values()) {
+ op.postUpdate(ctx);
+ }
+
+ for (RepoOnlyOp op : repoOnlyOps) {
+ op.postUpdate(ctx);
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
index 2b07280..cf4ea37 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
@@ -14,11 +14,11 @@
package com.google.gerrit.server.update;
-import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Comparator.comparing;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.stream.Collectors.toList;
import com.google.common.base.Stopwatch;
import com.google.common.base.Throwables;
@@ -30,7 +30,6 @@
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.metrics.Description;
import com.google.gerrit.metrics.Description.Units;
@@ -59,17 +58,12 @@
import com.google.gerrit.server.notedb.NoteDbUpdateManager.MismatchedStateException;
import com.google.gerrit.server.notedb.NotesMigration;
import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.NoSuchRefException;
import com.google.gerrit.server.util.RequestId;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
-import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
@@ -238,8 +232,8 @@
}
@Override
- public void bumpLastUpdatedOn(boolean bump) {
- bumpLastUpdatedOn = bump;
+ public void dontBumpLastUpdatedOn() {
+ bumpLastUpdatedOn = false;
}
@Override
@@ -273,16 +267,7 @@
if (updates.isEmpty()) {
return;
}
- if (requestId != null) {
- for (BatchUpdate u : updates) {
- checkArgument(
- u.requestId == null || u.requestId == requestId,
- "refusing to overwrite RequestId %s in update with %s",
- u.requestId,
- requestId);
- u.setRequestId(requestId);
- }
- }
+ setRequestIds(updates, requestId);
try {
Order order = getOrder(updates);
boolean updateChangesInParallel = getUpdateChangesInParallel(updates);
@@ -319,45 +304,26 @@
throw new IllegalStateException("invalid execution order: " + order);
}
- List<CheckedFuture<?, IOException>> indexFutures = new ArrayList<>();
- for (ReviewDbBatchUpdate u : updates) {
- indexFutures.addAll(u.indexFutures);
- }
- ChangeIndexer.allAsList(indexFutures).get();
+ ChangeIndexer.allAsList(
+ updates.stream().flatMap(u -> u.indexFutures.stream()).collect(toList()))
+ .get();
- for (ReviewDbBatchUpdate u : updates) {
- if (u.batchRefUpdate != null) {
- // Fire ref update events only after all mutations are finished, since
- // callers may assume a patch set ref being created means the change
- // was created, or a branch advancing meaning some changes were
- // closed.
- u.gitRefUpdated.fire(
- u.project,
- u.batchRefUpdate,
- u.getUser().isIdentifiedUser() ? u.getUser().asIdentifiedUser().getAccount() : null);
- }
- }
+ // Fire ref update events only after all mutations are finished, since callers may assume a
+ // patch set ref being created means the change was created, or a branch advancing meaning
+ // some changes were closed.
+ updates
+ .stream()
+ .filter(u -> u.batchRefUpdate != null)
+ .forEach(
+ u -> u.gitRefUpdated.fire(u.project, u.batchRefUpdate, u.getAccount().orElse(null)));
+
if (!dryrun) {
for (ReviewDbBatchUpdate u : updates) {
u.executePostOps();
}
}
- } catch (UpdateException | RestApiException e) {
- // Propagate REST API exceptions thrown by operations; they commonly throw
- // exceptions like ResourceConflictException to indicate an atomic update
- // failure.
- throw e;
-
- // Convert other common non-REST exception types with user-visible
- // messages to corresponding REST exception types
- } catch (InvalidChangeOperationException e) {
- throw new ResourceConflictException(e.getMessage(), e);
- } catch (NoSuchChangeException | NoSuchRefException | NoSuchProjectException e) {
- throw new ResourceNotFoundException(e.getMessage(), e);
-
} catch (Exception e) {
- Throwables.throwIfUnchecked(e);
- throw new UpdateException(e);
+ wrapAndThrowException(e);
}
}
@@ -376,7 +342,7 @@
private final long skewMs;
private final List<CheckedFuture<?, IOException>> indexFutures = new ArrayList<>();
- @AssistedInject
+ @Inject
ReviewDbBatchUpdate(
@GerritServerConfig Config cfg,
AllUsersName allUsers,
@@ -413,11 +379,6 @@
}
@Override
- public void execute() throws UpdateException, RestApiException {
- execute(BatchUpdateListener.NONE);
- }
-
- @Override
public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException {
execute(ImmutableList.of(this), listener, requestId, false);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
index 4d66809e..a1333c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -172,52 +172,46 @@
/** @see #wrap(Callable) */
protected abstract <T> Callable<T> wrapImpl(Callable<T> callable);
- protected <T> Callable<T> context(final RequestContext context, final Callable<T> callable) {
- return new Callable<T>() {
- @Override
- public T call() throws Exception {
- RequestContext old =
- local.setContext(
- new RequestContext() {
- @Override
- public CurrentUser getUser() {
- return context.getUser();
- }
+ protected <T> Callable<T> context(RequestContext context, Callable<T> callable) {
+ return () -> {
+ RequestContext old =
+ local.setContext(
+ new RequestContext() {
+ @Override
+ public CurrentUser getUser() {
+ return context.getUser();
+ }
- @Override
- public Provider<ReviewDb> getReviewDbProvider() {
- return dbProviderProvider.get();
- }
- });
- try {
- return callable.call();
- } finally {
- local.setContext(old);
- }
+ @Override
+ public Provider<ReviewDb> getReviewDbProvider() {
+ return dbProviderProvider.get();
+ }
+ });
+ try {
+ return callable.call();
+ } finally {
+ local.setContext(old);
}
};
}
- protected <T> Callable<T> cleanup(final Callable<T> callable) {
- return new Callable<T>() {
- @Override
- public T call() throws Exception {
- RequestCleanup cleanup =
- scope
- .scope(
- Key.get(RequestCleanup.class),
- new Provider<RequestCleanup>() {
- @Override
- public RequestCleanup get() {
- return new RequestCleanup();
- }
- })
- .get();
- try {
- return callable.call();
- } finally {
- cleanup.run();
- }
+ protected <T> Callable<T> cleanup(Callable<T> callable) {
+ return () -> {
+ RequestCleanup cleanup =
+ scope
+ .scope(
+ Key.get(RequestCleanup.class),
+ new Provider<RequestCleanup>() {
+ @Override
+ public RequestCleanup get() {
+ return new RequestCleanup();
+ }
+ })
+ .get();
+ try {
+ return callable.call();
+ } finally {
+ cleanup.run();
}
};
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
index 4b27208..90fb994 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
@@ -41,21 +41,18 @@
/** @see RequestScopePropagator#wrap(Callable) */
@Override
- protected final <T> Callable<T> wrapImpl(final Callable<T> callable) {
- final C ctx = continuingContext(requireContext());
- return new Callable<T>() {
- @Override
- public T call() throws Exception {
- C old = threadLocal.get();
- threadLocal.set(ctx);
- try {
- return callable.call();
- } finally {
- if (old != null) {
- threadLocal.set(old);
- } else {
- threadLocal.remove();
- }
+ protected final <T> Callable<T> wrapImpl(Callable<T> callable) {
+ C ctx = continuingContext(requireContext());
+ return () -> {
+ C old = threadLocal.get();
+ threadLocal.set(ctx);
+ try {
+ return callable.call();
+ } finally {
+ if (old != null) {
+ threadLocal.set(old);
+ } else {
+ threadLocal.remove();
}
}
};
diff --git a/gerrit-server/src/test/java/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java b/gerrit-server/src/test/java/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
index 9974bc6..91b01f6 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
@@ -126,19 +126,16 @@
@Test
public void callbackMetric0() {
- final CallbackMetric0<Long> cntr =
+ CallbackMetric0<Long> cntr =
metrics.newCallbackMetric(
"test/count", Long.class, new Description("simple test").setCumulative());
- final AtomicInteger invocations = new AtomicInteger(0);
+ AtomicInteger invocations = new AtomicInteger(0);
metrics.newTrigger(
cntr,
- new Runnable() {
- @Override
- public void run() {
- invocations.getAndIncrement();
- cntr.set(42L);
- }
+ () -> {
+ invocations.getAndIncrement();
+ cntr.set(42L);
});
// Triggers run immediately with DropWizard binding.
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java b/gerrit-server/src/test/java/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
new file mode 100644
index 0000000..801b2b0
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.edit.tree;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.io.CharStreams;
+import com.google.common.truth.FailureStrategy;
+import com.google.common.truth.StringSubject;
+import com.google.common.truth.Subject;
+import com.google.common.truth.SubjectFactory;
+import com.google.common.truth.Truth;
+import com.google.gerrit.extensions.restapi.RawInput;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+public class ChangeFileContentModificationSubject
+ extends Subject<ChangeFileContentModificationSubject, ChangeFileContentModification> {
+
+ private static final SubjectFactory<
+ ChangeFileContentModificationSubject, ChangeFileContentModification>
+ MODIFICATION_SUBJECT_FACTORY =
+ new SubjectFactory<
+ ChangeFileContentModificationSubject, ChangeFileContentModification>() {
+ @Override
+ public ChangeFileContentModificationSubject getSubject(
+ FailureStrategy failureStrategy, ChangeFileContentModification modification) {
+ return new ChangeFileContentModificationSubject(failureStrategy, modification);
+ }
+ };
+
+ public static ChangeFileContentModificationSubject assertThat(
+ ChangeFileContentModification modification) {
+ return assertAbout(MODIFICATION_SUBJECT_FACTORY).that(modification);
+ }
+
+ private ChangeFileContentModificationSubject(
+ FailureStrategy failureStrategy, ChangeFileContentModification modification) {
+ super(failureStrategy, modification);
+ }
+
+ public StringSubject filePath() {
+ isNotNull();
+ return Truth.assertThat(actual().getFilePath()).named("filePath");
+ }
+
+ public StringSubject newContent() throws IOException {
+ isNotNull();
+ RawInput newContent = actual().getNewContent();
+ Truth.assertThat(newContent).named("newContent").isNotNull();
+ String contentString =
+ CharStreams.toString(
+ new InputStreamReader(newContent.getInputStream(), StandardCharsets.UTF_8));
+ return Truth.assertThat(contentString).named("newContent");
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/edit/tree/TreeModificationSubject.java b/gerrit-server/src/test/java/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
new file mode 100644
index 0000000..ac4ebb8
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.edit.tree;
+
+import static com.google.common.truth.Truth.assertAbout;
+
+import com.google.common.truth.FailureStrategy;
+import com.google.common.truth.Subject;
+import com.google.common.truth.SubjectFactory;
+import com.google.gerrit.truth.ListSubject;
+import java.util.List;
+
+public class TreeModificationSubject extends Subject<TreeModificationSubject, TreeModification> {
+
+ private static final SubjectFactory<TreeModificationSubject, TreeModification>
+ TREE_MODIFICATION_SUBJECT_FACTORY =
+ new SubjectFactory<TreeModificationSubject, TreeModification>() {
+ @Override
+ public TreeModificationSubject getSubject(
+ FailureStrategy failureStrategy, TreeModification treeModification) {
+ return new TreeModificationSubject(failureStrategy, treeModification);
+ }
+ };
+
+ public static TreeModificationSubject assertThat(TreeModification treeModification) {
+ return assertAbout(TREE_MODIFICATION_SUBJECT_FACTORY).that(treeModification);
+ }
+
+ public static ListSubject<TreeModificationSubject, TreeModification> assertThatList(
+ List<TreeModification> treeModifications) {
+ return ListSubject.assertThat(treeModifications, TreeModificationSubject::assertThat)
+ .named("treeModifications");
+ }
+
+ private TreeModificationSubject(
+ FailureStrategy failureStrategy, TreeModification treeModification) {
+ super(failureStrategy, treeModification);
+ }
+
+ public ChangeFileContentModificationSubject asChangeFileContentModification() {
+ isInstanceOf(ChangeFileContentModification.class);
+ return ChangeFileContentModificationSubject.assertThat(
+ (ChangeFileContentModification) actual());
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java
new file mode 100644
index 0000000..c1a65bb
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java
@@ -0,0 +1,333 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.fixes;
+
+import static com.google.gerrit.server.edit.tree.TreeModificationSubject.assertThatList;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.replay;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.reviewdb.client.Comment.Range;
+import com.google.gerrit.reviewdb.client.FixReplacement;
+import com.google.gerrit.server.change.FileContentUtil;
+import com.google.gerrit.server.edit.tree.TreeModification;
+import com.google.gerrit.server.project.ProjectState;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import org.easymock.EasyMock;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class FixReplacementInterpreterTest {
+
+ @Rule public ExpectedException expectedException = ExpectedException.none();
+
+ private final FileContentUtil fileContentUtil = createMock(FileContentUtil.class);
+ private final Repository repository = createMock(Repository.class);
+ private final ProjectState projectState = createMock(ProjectState.class);
+ private final ObjectId patchSetCommitId = createMock(ObjectId.class);
+ private final String filePath1 = "an/arbitrary/file.txt";
+ private final String filePath2 = "another/arbitrary/file.txt";
+
+ private FixReplacementInterpreter fixReplacementInterpreter;
+
+ @Before
+ public void setUp() {
+ fixReplacementInterpreter = new FixReplacementInterpreter(fileContentUtil);
+ }
+
+ @Test
+ public void noReplacementsResultInNoTreeModifications() throws Exception {
+ List<TreeModification> treeModifications = toTreeModifications();
+ assertThatList(treeModifications).isEmpty();
+ }
+
+ @Test
+ public void treeModificationsTargetCorrectFiles() throws Exception {
+ FixReplacement fixReplacement =
+ new FixReplacement(filePath1, new Range(1, 6, 3, 2), "Modified content");
+ FixReplacement fixReplacement2 =
+ new FixReplacement(filePath1, new Range(3, 5, 3, 5), "Second modification");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+ FixReplacement fixReplacement3 =
+ new FixReplacement(filePath2, new Range(2, 0, 3, 0), "Another modified content");
+ mockFileContent(filePath2, "1st line\n2nd line\n3rd line\n");
+
+ replay(fileContentUtil);
+ List<TreeModification> treeModifications =
+ toTreeModifications(fixReplacement, fixReplacement3, fixReplacement2);
+ List<TreeModification> sortedTreeModifications = getSortedCopy(treeModifications);
+ assertThatList(sortedTreeModifications)
+ .element(0)
+ .asChangeFileContentModification()
+ .filePath()
+ .isEqualTo(filePath1);
+ assertThatList(sortedTreeModifications)
+ .element(0)
+ .asChangeFileContentModification()
+ .newContent()
+ .startsWith("First");
+ assertThatList(sortedTreeModifications)
+ .element(1)
+ .asChangeFileContentModification()
+ .filePath()
+ .isEqualTo(filePath2);
+ assertThatList(sortedTreeModifications)
+ .element(1)
+ .asChangeFileContentModification()
+ .newContent()
+ .startsWith("1st");
+ }
+
+ @Test
+ public void replacementsCanDeleteALine() throws Exception {
+ FixReplacement fixReplacement = new FixReplacement(filePath1, new Range(2, 0, 3, 0), "");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+ replay(fileContentUtil);
+ List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
+ assertThatList(treeModifications)
+ .onlyElement()
+ .asChangeFileContentModification()
+ .newContent()
+ .isEqualTo("First line\nThird line\n");
+ }
+
+ @Test
+ public void replacementsCanAddALine() throws Exception {
+ FixReplacement fixReplacement =
+ new FixReplacement(filePath1, new Range(2, 0, 2, 0), "A new line\n");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+ replay(fileContentUtil);
+ List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
+ assertThatList(treeModifications)
+ .onlyElement()
+ .asChangeFileContentModification()
+ .newContent()
+ .isEqualTo("First line\nA new line\nSecond line\nThird line\n");
+ }
+
+ @Test
+ public void replacementsMaySpanMultipleLines() throws Exception {
+ FixReplacement fixReplacement = new FixReplacement(filePath1, new Range(1, 6, 3, 1), "and t");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+ replay(fileContentUtil);
+ List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
+ assertThatList(treeModifications)
+ .onlyElement()
+ .asChangeFileContentModification()
+ .newContent()
+ .isEqualTo("First and third line\n");
+ }
+
+ @Test
+ public void replacementsMayOccurOnSameLine() throws Exception {
+ FixReplacement fixReplacement1 = new FixReplacement(filePath1, new Range(2, 0, 2, 6), "A");
+ FixReplacement fixReplacement2 =
+ new FixReplacement(filePath1, new Range(2, 7, 2, 11), "modification");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+ replay(fileContentUtil);
+ List<TreeModification> treeModifications =
+ toTreeModifications(fixReplacement1, fixReplacement2);
+ assertThatList(treeModifications)
+ .onlyElement()
+ .asChangeFileContentModification()
+ .newContent()
+ .isEqualTo("First line\nA modification\nThird line\n");
+ }
+
+ @Test
+ public void replacementsMayTouch() throws Exception {
+ FixReplacement fixReplacement1 =
+ new FixReplacement(filePath1, new Range(1, 6, 2, 7), "modified ");
+ FixReplacement fixReplacement2 =
+ new FixReplacement(filePath1, new Range(2, 7, 3, 5), "content");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+ replay(fileContentUtil);
+ List<TreeModification> treeModifications =
+ toTreeModifications(fixReplacement1, fixReplacement2);
+ assertThatList(treeModifications)
+ .onlyElement()
+ .asChangeFileContentModification()
+ .newContent()
+ .isEqualTo("First modified content line\n");
+ }
+
+ @Test
+ public void replacementsCanAddContentAtEndOfFile() throws Exception {
+ FixReplacement fixReplacement =
+ new FixReplacement(filePath1, new Range(4, 0, 4, 0), "New content");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+ replay(fileContentUtil);
+ List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
+ assertThatList(treeModifications)
+ .onlyElement()
+ .asChangeFileContentModification()
+ .newContent()
+ .isEqualTo("First line\nSecond line\nThird line\nNew content");
+ }
+
+ @Test
+ public void replacementsCanModifySeveralFilesInAnyOrder() throws Exception {
+ FixReplacement fixReplacement1 =
+ new FixReplacement(filePath1, new Range(1, 1, 3, 2), "Modified content");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+ FixReplacement fixReplacement2 =
+ new FixReplacement(filePath2, new Range(2, 0, 3, 0), "First modification\n");
+ FixReplacement fixReplacement3 =
+ new FixReplacement(filePath2, new Range(3, 0, 4, 0), "Second modification\n");
+ mockFileContent(filePath2, "1st line\n2nd line\n3rd line\n");
+
+ replay(fileContentUtil);
+ List<TreeModification> treeModifications =
+ toTreeModifications(fixReplacement3, fixReplacement1, fixReplacement2);
+ List<TreeModification> sortedTreeModifications = getSortedCopy(treeModifications);
+ assertThatList(sortedTreeModifications)
+ .element(0)
+ .asChangeFileContentModification()
+ .newContent()
+ .isEqualTo("FModified contentird line\n");
+ assertThatList(sortedTreeModifications)
+ .element(1)
+ .asChangeFileContentModification()
+ .newContent()
+ .isEqualTo("1st line\nFirst modification\nSecond modification\n");
+ }
+
+ @Test
+ public void lineSeparatorCanBeChanged() throws Exception {
+ FixReplacement fixReplacement = new FixReplacement(filePath1, new Range(2, 11, 3, 0), "\r");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+ replay(fileContentUtil);
+ List<TreeModification> treeModifications = toTreeModifications(fixReplacement);
+ assertThatList(treeModifications)
+ .onlyElement()
+ .asChangeFileContentModification()
+ .newContent()
+ .isEqualTo("First line\nSecond line\rThird line\n");
+ }
+
+ @Test
+ public void replacementsDoNotNeedToBeOrderedAccordingToRange() throws Exception {
+ FixReplacement fixReplacement1 =
+ new FixReplacement(filePath1, new Range(1, 0, 2, 0), "1st modification\n");
+ FixReplacement fixReplacement2 =
+ new FixReplacement(filePath1, new Range(3, 0, 4, 0), "2nd modification\n");
+ FixReplacement fixReplacement3 =
+ new FixReplacement(filePath1, new Range(4, 0, 5, 0), "3rd modification\n");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\nFourth line\nFifth line\n");
+
+ replay(fileContentUtil);
+ List<TreeModification> treeModifications =
+ toTreeModifications(fixReplacement2, fixReplacement1, fixReplacement3);
+ assertThatList(treeModifications)
+ .onlyElement()
+ .asChangeFileContentModification()
+ .newContent()
+ .isEqualTo(
+ "1st modification\nSecond line\n2nd modification\n3rd modification\nFifth line\n");
+ }
+
+ @Test
+ public void replacementsMustNotReferToNotExistingLine() throws Exception {
+ FixReplacement fixReplacement =
+ new FixReplacement(filePath1, new Range(5, 0, 5, 0), "A new line\n");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+ replay(fileContentUtil);
+
+ expectedException.expect(ResourceConflictException.class);
+ toTreeModifications(fixReplacement);
+ }
+
+ @Test
+ public void replacementsMustNotReferToZeroLine() throws Exception {
+ FixReplacement fixReplacement =
+ new FixReplacement(filePath1, new Range(0, 0, 0, 0), "A new line\n");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+ replay(fileContentUtil);
+
+ expectedException.expect(ResourceConflictException.class);
+ toTreeModifications(fixReplacement);
+ }
+
+ @Test
+ public void replacementsMustNotReferToNotExistingOffsetOfIntermediateLine() throws Exception {
+ FixReplacement fixReplacement =
+ new FixReplacement(filePath1, new Range(1, 0, 1, 11), "modified");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+ replay(fileContentUtil);
+
+ expectedException.expect(ResourceConflictException.class);
+ toTreeModifications(fixReplacement);
+ }
+
+ @Test
+ public void replacementsMustNotReferToNotExistingOffsetOfLastLine() throws Exception {
+ FixReplacement fixReplacement =
+ new FixReplacement(filePath1, new Range(3, 0, 3, 11), "modified");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+ replay(fileContentUtil);
+
+ expectedException.expect(ResourceConflictException.class);
+ toTreeModifications(fixReplacement);
+ }
+
+ @Test
+ public void replacementsMustNotReferToNegativeOffset() throws Exception {
+ FixReplacement fixReplacement =
+ new FixReplacement(filePath1, new Range(1, -1, 1, 5), "modified");
+ mockFileContent(filePath1, "First line\nSecond line\nThird line\n");
+
+ replay(fileContentUtil);
+
+ expectedException.expect(ResourceConflictException.class);
+ toTreeModifications(fixReplacement);
+ }
+
+ private void mockFileContent(String filePath, String fileContent) throws Exception {
+ EasyMock.expect(
+ fileContentUtil.getContent(repository, projectState, patchSetCommitId, filePath))
+ .andReturn(BinaryResult.create(fileContent));
+ }
+
+ private List<TreeModification> toTreeModifications(FixReplacement... fixReplacements)
+ throws Exception {
+ return fixReplacementInterpreter.toTreeModifications(
+ repository, projectState, patchSetCommitId, ImmutableList.copyOf(fixReplacements));
+ }
+
+ private static List<TreeModification> getSortedCopy(List<TreeModification> treeModifications) {
+ List<TreeModification> sortedTreeModifications = new ArrayList<>(treeModifications);
+ sortedTreeModifications.sort(Comparator.comparing(TreeModification::getFilePath));
+ return sortedTreeModifications;
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/fixes/LineIdentifierTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/fixes/LineIdentifierTest.java
new file mode 100644
index 0000000..f638346
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/fixes/LineIdentifierTest.java
@@ -0,0 +1,257 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.fixes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class LineIdentifierTest {
+
+ @Rule public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void lineNumberMustBePositive() {
+ LineIdentifier lineIdentifier = new LineIdentifier("First line\nSecond line");
+ expectedException.expect(StringIndexOutOfBoundsException.class);
+ expectedException.expectMessage("positive");
+ lineIdentifier.getStartIndexOfLine(0);
+ }
+
+ @Test
+ public void lineNumberMustIndicateAnAvailableLine() {
+ LineIdentifier lineIdentifier = new LineIdentifier("First line\nSecond line");
+ expectedException.expect(StringIndexOutOfBoundsException.class);
+ expectedException.expectMessage("Line 3 isn't available");
+ lineIdentifier.getStartIndexOfLine(3);
+ }
+
+ @Test
+ public void startIndexOfFirstLineIsRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567");
+ int startIndex = lineIdentifier.getStartIndexOfLine(1);
+ assertThat(startIndex).isEqualTo(0);
+ }
+
+ @Test
+ public void lengthOfFirstLineIsCorrect() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567");
+ int lineLength = lineIdentifier.getLengthOfLine(1);
+ assertThat(lineLength).isEqualTo(8);
+ }
+
+ @Test
+ public void startIndexOfSecondLineIsRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567");
+ int startIndex = lineIdentifier.getStartIndexOfLine(2);
+ assertThat(startIndex).isEqualTo(9);
+ }
+
+ @Test
+ public void lengthOfSecondLineIsCorrect() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567");
+ int lineLength = lineIdentifier.getLengthOfLine(2);
+ assertThat(lineLength).isEqualTo(3);
+ }
+
+ @Test
+ public void startIndexOfLastLineIsRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567");
+ int startIndex = lineIdentifier.getStartIndexOfLine(3);
+ assertThat(startIndex).isEqualTo(13);
+ }
+
+ @Test
+ public void lengthOfLastLineIsCorrect() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567");
+ int lineLength = lineIdentifier.getLengthOfLine(3);
+ assertThat(lineLength).isEqualTo(7);
+ }
+
+ @Test
+ public void emptyFirstLineIsRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("\n123\n1234567");
+ int startIndex = lineIdentifier.getStartIndexOfLine(1);
+ assertThat(startIndex).isEqualTo(0);
+ }
+
+ @Test
+ public void lengthOfEmptyFirstLineIsCorrect() {
+ LineIdentifier lineIdentifier = new LineIdentifier("\n123\n1234567");
+ int lineLength = lineIdentifier.getLengthOfLine(1);
+ assertThat(lineLength).isEqualTo(0);
+ }
+
+ @Test
+ public void emptyIntermediaryLineIsRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n\n1234567");
+ int startIndex = lineIdentifier.getStartIndexOfLine(2);
+ assertThat(startIndex).isEqualTo(9);
+ }
+
+ @Test
+ public void lengthOfEmptyIntermediaryLineIsCorrect() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n\n1234567");
+ int lineLength = lineIdentifier.getLengthOfLine(2);
+ assertThat(lineLength).isEqualTo(0);
+ }
+
+ @Test
+ public void lineAfterIntermediaryLineIsRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n\n1234567");
+ int startIndex = lineIdentifier.getStartIndexOfLine(3);
+ assertThat(startIndex).isEqualTo(10);
+ }
+
+ @Test
+ public void emptyLastLineIsRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n");
+ int startIndex = lineIdentifier.getStartIndexOfLine(3);
+ assertThat(startIndex).isEqualTo(13);
+ }
+
+ @Test
+ public void lengthOfEmptyLastLineIsCorrect() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n");
+ int lineLength = lineIdentifier.getLengthOfLine(3);
+ assertThat(lineLength).isEqualTo(0);
+ }
+
+ @Test
+ public void startIndexOfSingleLineIsRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678");
+ int startIndex = lineIdentifier.getStartIndexOfLine(1);
+ assertThat(startIndex).isEqualTo(0);
+ }
+
+ @Test
+ public void lengthOfSingleLineIsCorrect() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678");
+ int lineLength = lineIdentifier.getLengthOfLine(1);
+ assertThat(lineLength).isEqualTo(8);
+ }
+
+ @Test
+ public void startIndexOfSingleEmptyLineIsRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("");
+ int startIndex = lineIdentifier.getStartIndexOfLine(1);
+ assertThat(startIndex).isEqualTo(0);
+ }
+
+ @Test
+ public void lengthOfSingleEmptyLineIsCorrect() {
+ LineIdentifier lineIdentifier = new LineIdentifier("");
+ int lineLength = lineIdentifier.getLengthOfLine(1);
+ assertThat(lineLength).isEqualTo(0);
+ }
+
+ @Test
+ public void lookingUpSubsequentLinesIsPossible() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567\n12");
+
+ int firstLineStartIndex = lineIdentifier.getStartIndexOfLine(1);
+ assertThat(firstLineStartIndex).isEqualTo(0);
+
+ int secondLineStartIndex = lineIdentifier.getStartIndexOfLine(2);
+ assertThat(secondLineStartIndex).isEqualTo(9);
+ }
+
+ @Test
+ public void lookingUpNotSubsequentLinesInAscendingOrderIsPossible() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567\n12");
+
+ int firstLineStartIndex = lineIdentifier.getStartIndexOfLine(1);
+ assertThat(firstLineStartIndex).isEqualTo(0);
+
+ int fourthLineStartIndex = lineIdentifier.getStartIndexOfLine(4);
+ assertThat(fourthLineStartIndex).isEqualTo(21);
+ }
+
+ @Test
+ public void lookingUpNotSubsequentLinesInDescendingOrderIsPossible() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567\n12");
+
+ int fourthLineStartIndex = lineIdentifier.getStartIndexOfLine(4);
+ assertThat(fourthLineStartIndex).isEqualTo(21);
+
+ int secondLineStartIndex = lineIdentifier.getStartIndexOfLine(2);
+ assertThat(secondLineStartIndex).isEqualTo(9);
+ }
+
+ @Test
+ public void linesSeparatedByOnlyCarriageReturnAreRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\r123\r12");
+ int startIndex = lineIdentifier.getStartIndexOfLine(2);
+ assertThat(startIndex).isEqualTo(9);
+ }
+
+ @Test
+ public void lengthOfLinesSeparatedByOnlyCarriageReturnIsCorrect() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\r123\r12");
+ int lineLength = lineIdentifier.getLengthOfLine(2);
+ assertThat(lineLength).isEqualTo(3);
+ }
+
+ @Test
+ public void linesSeparatedByLineFeedAndCarriageReturnAreRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\r\n123\r\n12");
+ int startIndex = lineIdentifier.getStartIndexOfLine(2);
+ assertThat(startIndex).isEqualTo(10);
+ }
+
+ @Test
+ public void lengthOfLinesSeparatedByLineFeedAndCarriageReturnIsCorrect() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\r\n123\r\n12");
+ int lineLength = lineIdentifier.getLengthOfLine(2);
+ assertThat(lineLength).isEqualTo(3);
+ }
+
+ @Test
+ public void linesSeparatedByMixtureOfCarriageReturnAndLineFeedAreRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\r123\r\n12\n123456\r\n1234");
+ int startIndex = lineIdentifier.getStartIndexOfLine(5);
+ assertThat(startIndex).isEqualTo(25);
+ }
+
+ @Test
+ public void linesSeparatedBySomeUnicodeLinebreakCharacterAreRecognized() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\u2029123\u202912");
+ int startIndex = lineIdentifier.getStartIndexOfLine(2);
+ assertThat(startIndex).isEqualTo(9);
+ }
+
+ @Test
+ public void lengthOfLinesSeparatedBySomeUnicodeLinebreakCharacterIsCorrect() {
+ LineIdentifier lineIdentifier = new LineIdentifier("12345678\u2029123\u202912");
+ int lineLength = lineIdentifier.getLengthOfLine(2);
+ assertThat(lineLength).isEqualTo(3);
+ }
+
+ @Test
+ public void blanksAreNotInterpretedAsLineSeparators() {
+ LineIdentifier lineIdentifier = new LineIdentifier("1 2345678\n123\n12");
+ int startIndex = lineIdentifier.getStartIndexOfLine(2);
+ assertThat(startIndex).isEqualTo(10);
+ }
+
+ @Test
+ public void tabsAreNotInterpretedAsLineSeparators() {
+ LineIdentifier lineIdentifier = new LineIdentifier("123\t45678\n123\n12");
+ int startIndex = lineIdentifier.getStartIndexOfLine(2);
+ assertThat(startIndex).isEqualTo(10);
+ }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/fixes/StringModifierTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/fixes/StringModifierTest.java
new file mode 100644
index 0000000..d23e928
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/fixes/StringModifierTest.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.fixes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class StringModifierTest {
+
+ @Rule public ExpectedException expectedException = ExpectedException.none();
+
+ private final String originalString = "This is the original, unmodified string.";
+ private StringModifier stringModifier;
+
+ @Before
+ public void setUp() {
+ stringModifier = new StringModifier(originalString);
+ }
+
+ @Test
+ public void singlePartIsReplaced() {
+ stringModifier.replace(0, 11, "An");
+ String modifiedString = stringModifier.getResult();
+ assertThat(modifiedString).isEqualTo("An original, unmodified string.");
+ }
+
+ @Test
+ public void twoPartsCanBeReplacedWithInsertionFirst() {
+ stringModifier.replace(5, 5, "string ");
+ stringModifier.replace(8, 39, "a modified version");
+ String modifiedString = stringModifier.getResult();
+ assertThat(modifiedString).isEqualTo("This string is a modified version.");
+ }
+
+ @Test
+ public void twoPartsCanBeReplacedWithDeletionFirst() {
+ stringModifier.replace(0, 8, "");
+ stringModifier.replace(12, 32, "modified");
+ String modifiedString = stringModifier.getResult();
+ assertThat(modifiedString).isEqualTo("the modified string.");
+ }
+
+ @Test
+ public void replacedPartsMayTouch() {
+ stringModifier.replace(0, 8, "");
+ stringModifier.replace(8, 32, "The modified");
+ String modifiedString = stringModifier.getResult();
+ assertThat(modifiedString).isEqualTo("The modified string.");
+ }
+
+ @Test
+ public void replacedPartsMustNotOverlap() {
+ stringModifier.replace(0, 9, "");
+ expectedException.expect(StringIndexOutOfBoundsException.class);
+ stringModifier.replace(8, 32, "The modified");
+ }
+
+ @Test
+ public void startIndexMustNotBeGreaterThanEndIndex() {
+ expectedException.expect(StringIndexOutOfBoundsException.class);
+ stringModifier.replace(10, 9, "something");
+ }
+
+ @Test
+ public void startIndexMustNotBeNegative() {
+ expectedException.expect(StringIndexOutOfBoundsException.class);
+ stringModifier.replace(-1, 9, "something");
+ }
+
+ @Test
+ public void newContentCanBeInsertedAtEndOfString() {
+ stringModifier.replace(
+ originalString.length(), originalString.length(), " And this an addition.");
+ String modifiedString = stringModifier.getResult();
+ assertThat(modifiedString)
+ .isEqualTo("This is the original, unmodified string. And this an addition.");
+ }
+
+ @Test
+ public void startIndexMustNotBeGreaterThanLengthOfString() {
+ expectedException.expect(StringIndexOutOfBoundsException.class);
+ stringModifier.replace(originalString.length() + 1, originalString.length() + 1, "something");
+ }
+
+ @Test
+ public void endIndexMustNotBeGreaterThanLengthOfString() {
+ expectedException.expect(StringIndexOutOfBoundsException.class);
+ stringModifier.replace(8, originalString.length() + 1, "something");
+ }
+}
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 9d6cb60..5a1d10c 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
@@ -49,6 +49,7 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.config.GerritServerId;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.gerrit.server.util.RequestId;
import com.google.gerrit.testutil.TestChanges;
@@ -751,7 +752,7 @@
try (RevWalk walk = new RevWalk(repo)) {
RevCommit commit = walk.parseCommit(update.getResult());
walk.parseBody(commit);
- assertThat(commit.getFullMessage()).endsWith("Hashtags: tag1,tag2\n");
+ assertThat(commit.getFullMessage()).contains("Hashtags: tag1,tag2\n");
}
}
@@ -3265,6 +3266,138 @@
assertThat(notes.getReadOnlyUntil()).isEqualTo(new Timestamp(0));
}
+ @Test
+ public void privateDefault() throws Exception {
+ Change c = newChange();
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.isPrivate()).isFalse();
+ }
+
+ @Test
+ public void privateSetPrivate() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.setPrivate(true);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.isPrivate()).isTrue();
+ }
+
+ @Test
+ public void privateSetPrivateMultipleTimes() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.setPrivate(true);
+ update.commit();
+
+ update = newUpdate(c, changeOwner);
+ update.setPrivate(false);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.isPrivate()).isFalse();
+ }
+
+ @Test
+ public void defaultReviewersByEmailIsEmpty() throws Exception {
+ Change c = newChange();
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getReviewersByEmail().all()).isEmpty();
+ }
+
+ @Test
+ public void putReviewerByEmail() throws Exception {
+ Address adr = new Address("Foo Bar", "foo.bar@gerritcodereview.com");
+
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putReviewerByEmail(adr, ReviewerStateInternal.REVIEWER);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getReviewersByEmail().all()).containsExactly(adr);
+ }
+
+ @Test
+ public void putAndRemoveReviewerByEmail() throws Exception {
+ Address adr = new Address("Foo Bar", "foo.bar@gerritcodereview.com");
+
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putReviewerByEmail(adr, ReviewerStateInternal.REVIEWER);
+ update.commit();
+
+ update = newUpdate(c, changeOwner);
+ update.removeReviewerByEmail(adr);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getReviewersByEmail().all()).isEmpty();
+ }
+
+ @Test
+ public void putRemoveAndAddBackReviewerByEmail() throws Exception {
+ Address adr = new Address("Foo Bar", "foo.bar@gerritcodereview.com");
+
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putReviewerByEmail(adr, ReviewerStateInternal.REVIEWER);
+ update.commit();
+
+ update = newUpdate(c, changeOwner);
+ update.removeReviewerByEmail(adr);
+ update.commit();
+
+ update = newUpdate(c, changeOwner);
+ update.putReviewerByEmail(adr, ReviewerStateInternal.CC);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getReviewersByEmail().all()).containsExactly(adr);
+ }
+
+ @Test
+ public void putReviewerByEmailAndCcByEmail() throws Exception {
+ Address adrReviewer = new Address("Foo Bar", "foo.bar@gerritcodereview.com");
+ Address adrCc = new Address("Foo Bor", "foo.bar.2@gerritcodereview.com");
+
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putReviewerByEmail(adrReviewer, ReviewerStateInternal.REVIEWER);
+ update.commit();
+
+ update = newUpdate(c, changeOwner);
+ update.putReviewerByEmail(adrCc, ReviewerStateInternal.CC);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getReviewersByEmail().byState(ReviewerStateInternal.REVIEWER))
+ .containsExactly(adrReviewer);
+ assertThat(notes.getReviewersByEmail().byState(ReviewerStateInternal.CC))
+ .containsExactly(adrCc);
+ assertThat(notes.getReviewersByEmail().all()).containsExactly(adrReviewer, adrCc);
+ }
+
+ @Test
+ public void putReviewerByEmailAndChangeToCc() throws Exception {
+ Address adr = new Address("Foo Bar", "foo.bar@gerritcodereview.com");
+
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putReviewerByEmail(adr, ReviewerStateInternal.REVIEWER);
+ update.commit();
+
+ update = newUpdate(c, changeOwner);
+ update.putReviewerByEmail(adr, ReviewerStateInternal.CC);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ assertThat(notes.getReviewersByEmail().byState(ReviewerStateInternal.REVIEWER)).isEmpty();
+ assertThat(notes.getReviewersByEmail().byState(ReviewerStateInternal.CC)).containsExactly(adr);
+ assertThat(notes.getReviewersByEmail().all()).containsExactly(adr);
+ }
+
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 25b5168..83dcf61 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
@@ -23,6 +23,7 @@
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.mail.Address;
import com.google.gerrit.server.util.RequestId;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.TestChanges;
@@ -382,6 +383,32 @@
assertBodyEquals("Update patch set 1\n\nPatch-set: 1\nCurrent: true\n", update.getResult());
}
+ @Test
+ public void reviewerByEmail() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putReviewerByEmail(
+ new Address("John Doe", "j.doe@gerritcodereview.com"), ReviewerStateInternal.REVIEWER);
+ update.commit();
+
+ assertBodyEquals(
+ "Update patch set 1\n\nPatch-set: 1\n"
+ + "Reviewer-email: John Doe <j.doe@gerritcodereview.com>\n",
+ update.getResult());
+ }
+
+ @Test
+ public void ccByEmail() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, changeOwner);
+ update.putReviewerByEmail(new Address("j.doe@gerritcodereview.com"), ReviewerStateInternal.CC);
+ update.commit();
+
+ assertBodyEquals(
+ "Update patch set 1\n\nPatch-set: 1\nCC-email: j.doe@gerritcodereview.com\n",
+ update.getResult());
+ }
+
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/RepoSequenceTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java
index df3e405..6977ce2 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -159,14 +159,11 @@
// Seed existing ref value.
writeBlob("id", "1");
- final AtomicBoolean doneBgUpdate = new AtomicBoolean(false);
+ AtomicBoolean doneBgUpdate = new AtomicBoolean(false);
Runnable bgUpdate =
- new Runnable() {
- @Override
- public void run() {
- if (!doneBgUpdate.getAndSet(true)) {
- writeBlob("id", "1234");
- }
+ () -> {
+ if (!doneBgUpdate.getAndSet(true)) {
+ writeBlob("id", "1234");
}
};
@@ -203,20 +200,13 @@
@Test
public void failAfterRetryerGivesUp() throws Exception {
- final AtomicInteger bgCounter = new AtomicInteger(1234);
- Runnable bgUpdate =
- new Runnable() {
- @Override
- public void run() {
- writeBlob("id", Integer.toString(bgCounter.getAndAdd(1000)));
- }
- };
+ AtomicInteger bgCounter = new AtomicInteger(1234);
RepoSequence s =
newSequence(
"id",
1,
10,
- bgUpdate,
+ () -> writeBlob("id", Integer.toString(bgCounter.getAndAdd(1000))),
RetryerBuilder.<RefUpdate.Result>newBuilder()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build());
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 cd179b5..35a9ebc 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
@@ -44,6 +44,8 @@
import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
import com.google.gerrit.extensions.api.changes.StarsInput;
import com.google.gerrit.extensions.api.groups.GroupInput;
+import com.google.gerrit.extensions.api.projects.ConfigInput;
+import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
@@ -71,6 +73,7 @@
import com.google.gerrit.server.change.ChangeTriplet;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.index.IndexConfig;
import com.google.gerrit.server.index.QueryOptions;
@@ -83,6 +86,7 @@
import com.google.gerrit.server.notedb.NoteDbChangeState;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.schema.SchemaCreator;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.util.RequestContext;
@@ -94,6 +98,7 @@
import com.google.gerrit.testutil.InMemoryRepositoryManager;
import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
import com.google.gerrit.testutil.TestTimeUtil;
+import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
@@ -101,6 +106,7 @@
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
@@ -111,6 +117,7 @@
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
@@ -140,7 +147,6 @@
@Inject protected ChangeIndexCollection indexes;
@Inject protected ChangeIndexer indexer;
@Inject protected IndexConfig indexConfig;
- @Inject protected InMemoryDatabase schemaFactory;
@Inject protected InMemoryRepositoryManager repoManager;
@Inject protected InternalChangeQuery internalChangeQuery;
@Inject protected ChangeNotes.Factory notesFactory;
@@ -149,8 +155,14 @@
@Inject protected ChangeControl.GenericFactory changeControlFactory;
@Inject protected ChangeQueryProcessor queryProcessor;
@Inject protected SchemaCreator schemaCreator;
+ @Inject protected SchemaFactory<ReviewDb> schemaFactory;
@Inject protected Sequences seq;
@Inject protected ThreadLocalRequestContext requestContext;
+ @Inject protected ProjectCache projectCache;
+ @Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
+
+ // Only for use in setting up/tearing down injector; other users should use schemaFactory.
+ @Inject private InMemoryDatabase inMemoryDatabase;
protected Injector injector;
protected LifecycleManager lifecycle;
@@ -173,8 +185,10 @@
}
protected void setUpDatabase() throws Exception {
+ try (ReviewDb underlyingDb = inMemoryDatabase.getDatabase().open()) {
+ schemaCreator.create(underlyingDb);
+ }
db = schemaFactory.open();
- schemaCreator.create(db);
userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId();
Account userAccount = db.accounts().get(userId);
@@ -208,7 +222,7 @@
if (db != null) {
db.close();
}
- InMemoryDatabase.drop(schemaFactory);
+ InMemoryDatabase.drop(inMemoryDatabase);
}
@Before
@@ -373,11 +387,35 @@
}
@Test
+ public void byPrivate() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(repo, newChange(repo), userId);
+ Account.Id user2 =
+ accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId();
+ Change change2 = insert(repo, newChange(repo), user2);
+
+ // No private changes.
+ assertQuery("is:open", change2, change1);
+ assertQuery("is:private");
+
+ gApi.changes().id(change1.getChangeId()).setPrivate(true);
+
+ // Change1 is not private, but should be still visible to its owner.
+ assertQuery("is:open", change1, change2);
+ assertQuery("is:private", change1);
+
+ // Switch request context to user2.
+ requestContext.setContext(newRequestContext(user2));
+ assertQuery("is:open", change2);
+ assertQuery("is:private");
+ }
+
+ @Test
public void byCommit() throws Exception {
TestRepository<Repo> repo = createProject("repo");
ChangeInserter ins = newChange(repo);
insert(repo, ins);
- String sha = ins.getCommit().name();
+ String sha = ins.getCommitId().name();
assertQuery("0000000000000000000000000000000000000000");
for (int i = 0; i <= 36; i++) {
@@ -536,8 +574,23 @@
assertQuery("intopic:fixup", change4);
assertQuery("topic:\"\"", change5);
assertQuery("intopic:\"\"", change5);
- assertQuery("intopic:^feature2.*", change4, change2);
- assertQuery("intopic:{^.*feature2$}", change3, change2);
+ }
+
+ @Test
+ public void byTopicRegex() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+
+ ChangeInserter ins1 = newChangeWithTopic(repo, "feature1");
+ Change change1 = insert(repo, ins1);
+
+ ChangeInserter ins2 = newChangeWithTopic(repo, "Cherrypick-feature1");
+ Change change2 = insert(repo, ins2);
+
+ ChangeInserter ins3 = newChangeWithTopic(repo, "feature1-fixup");
+ Change change3 = insert(repo, ins3);
+
+ assertQuery("intopic:^feature1.*", change3, change1);
+ assertQuery("intopic:{^.*feature1$}", change2, change1);
}
@Test
@@ -1343,7 +1396,8 @@
allUsers.update(draftsRef.getName(), draftsRef.getObjectId());
assertThat(allUsers.getRepository().exactRef(draftsRef.getName())).isNotNull();
- if (PrimaryStorage.of(change) == PrimaryStorage.REVIEW_DB) {
+ if (PrimaryStorage.of(change) == PrimaryStorage.REVIEW_DB
+ && !notesMigration.disableChangeReviewDb()) {
// Record draft ref in noteDbState as well.
ReviewDb db = ReviewDbUtil.unwrapDb(this.db);
change = db.changes().get(id);
@@ -1523,6 +1577,71 @@
}
@Test
+ public void reviewerAndCcByEmail() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ Project.NameKey project = new Project.NameKey("repo");
+ TestRepository<Repo> repo = createProject(project.get());
+ ConfigInput conf = new ConfigInput();
+ conf.enableReviewerByEmail = InheritableBoolean.TRUE;
+ gApi.projects().name(project.get()).config(conf);
+
+ String userByEmail = "un.registered@reviewer.com";
+ String userByEmailWithName = "John Doe <" + userByEmail + ">";
+
+ Change change1 = insert(repo, newChange(repo));
+ Change change2 = insert(repo, newChange(repo));
+ insert(repo, newChange(repo));
+
+ AddReviewerInput rin = new AddReviewerInput();
+ rin.reviewer = userByEmailWithName;
+ rin.state = ReviewerState.REVIEWER;
+ gApi.changes().id(change1.getId().get()).addReviewer(rin);
+
+ rin = new AddReviewerInput();
+ rin.reviewer = userByEmailWithName;
+ rin.state = ReviewerState.CC;
+ gApi.changes().id(change2.getId().get()).addReviewer(rin);
+
+ assertQuery("reviewer:\"" + userByEmailWithName + "\"", change1);
+ assertQuery("cc:\"" + userByEmailWithName + "\"", change2);
+
+ // Omitting the name:
+ assertQuery("reviewer:\"" + userByEmail + "\"", change1);
+ assertQuery("cc:\"" + userByEmail + "\"", change2);
+ }
+
+ @Test
+ public void reviewerAndCcByEmailWithQueryForDifferentUser() throws Exception {
+ assume().that(notesMigration.enabled()).isTrue();
+
+ Project.NameKey project = new Project.NameKey("repo");
+ TestRepository<Repo> repo = createProject(project.get());
+ ConfigInput conf = new ConfigInput();
+ conf.enableReviewerByEmail = InheritableBoolean.TRUE;
+ gApi.projects().name(project.get()).config(conf);
+
+ String userByEmail = "John Doe <un.registered@reviewer.com>";
+
+ Change change1 = insert(repo, newChange(repo));
+ Change change2 = insert(repo, newChange(repo));
+ insert(repo, newChange(repo));
+
+ AddReviewerInput rin = new AddReviewerInput();
+ rin.reviewer = userByEmail;
+ rin.state = ReviewerState.REVIEWER;
+ gApi.changes().id(change1.getId().get()).addReviewer(rin);
+
+ rin = new AddReviewerInput();
+ rin.reviewer = userByEmail;
+ rin.state = ReviewerState.CC;
+ gApi.changes().id(change2.getId().get()).addReviewer(rin);
+
+ assertQuery("reviewer:\"someone@example.com\"");
+ assertQuery("cc:\"someone@example.com\"");
+ }
+
+ @Test
public void submitRecords() throws Exception {
Account.Id user1 = createAccount("user1");
TestRepository<Repo> repo = createProject("repo");
@@ -1610,9 +1729,28 @@
@Test
public void byCommitsOnBranchNotMerged() throws Exception {
+ TestRepository<Repo> tr = createProject("repo");
+ testByCommitsOnBranchNotMerged(tr, ImmutableSet.of());
+ }
+
+ @Test
+ public void byCommitsOnBranchNotMergedSkipsMissingChanges() throws Exception {
TestRepository<Repo> repo = createProject("repo");
+ ObjectId missing =
+ repo.branch(new PatchSet.Id(new Change.Id(987654), 1).toRefName())
+ .commit()
+ .message("No change for this commit")
+ .insertChangeId()
+ .create()
+ .copy();
+ testByCommitsOnBranchNotMerged(repo, ImmutableSet.of(missing));
+ }
+
+ private void testByCommitsOnBranchNotMerged(TestRepository<Repo> repo, Collection<ObjectId> extra)
+ throws Exception {
int n = 10;
- List<String> shas = new ArrayList<>(n);
+ List<String> shas = new ArrayList<>(n + extra.size());
+ extra.forEach(i -> shas.add(i.name()));
List<Integer> expectedIds = new ArrayList<>(n);
Branch.NameKey dest = null;
for (int i = 0; i < n; i++) {
@@ -1621,7 +1759,7 @@
if (dest == null) {
dest = ins.getChange().getDest();
}
- shas.add(ins.getCommit().name());
+ shas.add(ins.getCommitId().name());
expectedIds.add(ins.getChange().getId().get());
}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/update/BatchUpdateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/update/BatchUpdateTest.java
index 892d037..5e82836 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -1,3 +1,17 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
package com.google.gerrit.server.update;
import static org.junit.Assert.assertEquals;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
index 9944508..4ab7018 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -59,6 +59,8 @@
import com.google.gerrit.server.patch.DiffExecutor;
import com.google.gerrit.server.schema.DataSourceType;
import com.google.gerrit.server.schema.H2AccountPatchReviewStore;
+import com.google.gerrit.server.schema.NotesMigrationSchemaFactory;
+import com.google.gerrit.server.schema.ReviewDbFactory;
import com.google.gerrit.server.schema.SchemaCreator;
import com.google.gerrit.server.securestore.DefaultSecureStore;
import com.google.gerrit.server.securestore.SecureStore;
@@ -69,6 +71,7 @@
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
+import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.Provides;
@@ -171,13 +174,15 @@
bind(ListeningExecutorService.class)
.annotatedWith(ChangeUpdateExecutor.class)
.toInstance(MoreExecutors.newDirectExecutorService());
-
bind(DataSourceType.class).to(InMemoryH2Type.class);
- bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).to(InMemoryDatabase.class);
bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class);
-
bind(SecureStore.class).to(DefaultSecureStore.class);
+ TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory =
+ new TypeLiteral<SchemaFactory<ReviewDb>>() {};
+ bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
+ bind(Key.get(schemaFactory, ReviewDbFactory.class)).to(InMemoryDatabase.class);
+
install(NoSshKeyCache.module());
install(
new CanonicalWebUrlModule() {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
index aeaaa47..ad876ce 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
@@ -89,7 +89,7 @@
List<ChangeBundle> allExpected = readExpected(changeIds);
- boolean oldWrite = notesMigration.writeChanges();
+ boolean oldWrite = notesMigration.rawWriteChangesSetting();
boolean oldRead = notesMigration.readChanges();
try {
notesMigration.setWriteChanges(true);
@@ -162,7 +162,7 @@
private void checkActual(List<ChangeBundle> allExpected, List<String> msgs) throws Exception {
ReviewDb db = getUnwrappedDb();
boolean oldRead = notesMigration.readChanges();
- boolean oldWrite = notesMigration.writeChanges();
+ boolean oldWrite = notesMigration.rawWriteChangesSetting();
try {
notesMigration.setWriteChanges(true);
notesMigration.setReadChanges(true);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
index e6a72fc..d4ddaf1 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
@@ -29,6 +29,8 @@
private volatile boolean disableChangeReviewDb;
private volatile boolean failOnLoad;
+ public TestNotesMigration() {}
+
@Override
public boolean readChanges() {
return readChanges;
@@ -54,7 +56,7 @@
// Increase visbility from superclass, as tests may want to check whether
// NoteDb data is written in specific migration scenarios.
@Override
- public boolean writeChanges() {
+ public boolean rawWriteChangesSetting() {
return writeChanges;
}
@@ -129,4 +131,12 @@
}
return this;
}
+
+ public TestNotesMigration setFrom(NotesMigration other) {
+ setWriteChanges(other.rawWriteChangesSetting());
+ setReadChanges(other.readChanges());
+ setChangePrimaryStorage(other.changePrimaryStorage());
+ setDisableChangeReviewDb(other.disableChangeReviewDb());
+ return this;
+ }
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index bc465ec..b828a58 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -20,8 +20,10 @@
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.DynamicOptions;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RequestCleanup;
import com.google.gerrit.server.git.ProjectRunnable;
@@ -84,6 +86,8 @@
@Inject private SshScope.Context context;
+ @Inject private DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
+
/** Commands declared by a plugin can be scoped by the plugin name. */
@Inject(optional = true)
@PluginName
@@ -193,6 +197,7 @@
*/
protected void parseCommandLine(Object options) throws UnloggedFailure {
final CmdLineParser clp = newCmdLineParser(options);
+ DynamicOptions.parse(dynamicBeans, clp, options);
try {
clp.parseArgument(argv);
} catch (IllegalArgumentException | CmdLineException err) {
@@ -224,31 +229,6 @@
* <p>Typically this should be invoked within {@link Command#start(Environment)}, such as:
*
* <pre>
- * startThread(new Runnable() {
- * public void run() {
- * runImp();
- * }
- * });
- * </pre>
- *
- * @param thunk the runnable to execute on the thread, performing the command's logic.
- */
- protected void startThread(final Runnable thunk) {
- startThread(
- new CommandRunnable() {
- @Override
- public void run() throws Exception {
- thunk.run();
- }
- });
- }
-
- /**
- * Spawn a function into its own thread.
- *
- * <p>Typically this should be invoked within {@link Command#start(Environment)}, such as:
- *
- * <pre>
* startThread(new CommandRunnable() {
* public void run() throws Exception {
* runImp();
@@ -469,6 +449,7 @@
}
/** Runnable function which can throw an exception. */
+ @FunctionalInterface
public interface CommandRunnable {
void run() throws Exception;
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index c6d750c..66b8fe6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -230,13 +230,7 @@
Future<?> future = task.getAndSet(null);
if (future != null) {
future.cancel(true);
- destroyExecutor.execute(
- new Runnable() {
- @Override
- public void run() {
- onDestroy();
- }
- });
+ destroyExecutor.execute(this::onDestroy);
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 837865e..a3cf7c1 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -14,14 +14,15 @@
package com.google.gerrit.sshd;
-import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gerrit.server.ssh.SshKeyCreator;
@@ -92,22 +93,23 @@
static class Loader extends CacheLoader<String, Iterable<SshKeyCacheEntry>> {
private final SchemaFactory<ReviewDb> schema;
+ private final ExternalIds externalIds;
private final VersionedAuthorizedKeys.Accessor authorizedKeys;
@Inject
- Loader(SchemaFactory<ReviewDb> schema, VersionedAuthorizedKeys.Accessor authorizedKeys) {
+ Loader(
+ SchemaFactory<ReviewDb> schema,
+ ExternalIds externalIds,
+ VersionedAuthorizedKeys.Accessor authorizedKeys) {
this.schema = schema;
+ this.externalIds = externalIds;
this.authorizedKeys = authorizedKeys;
}
@Override
public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
try (ReviewDb db = schema.open()) {
- ExternalId user =
- ExternalId.from(
- db.accountExternalIds()
- .get(
- ExternalId.Key.create(SCHEME_USERNAME, username).asAccountExternalIdKey()));
+ ExternalId user = externalIds.get(db, ExternalId.Key.create(SCHEME_USERNAME, username));
if (user == null) {
return NO_SUCH_USER;
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index d25c58b..789a630 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -17,7 +17,9 @@
import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
import static com.google.inject.Scopes.SINGLETON;
+import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.DynamicOptions;
import com.google.gerrit.server.PeerDaemonUser;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.config.GerritRequestModule;
@@ -94,6 +96,8 @@
.annotatedWith(UniqueAnnotations.create())
.to(SshPluginStarterCallback.class);
+ DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class);
+
listener().toInstance(registerInParentInjectors());
listener().to(SshLog.class);
listener().to(SshDaemon.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
index 7f76ec6..9ae1814 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -14,6 +14,8 @@
package com.google.gerrit.sshd;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.server.DynamicOptions;
import com.google.gerrit.server.plugins.Plugin;
import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.plugins.StartPluginListener;
@@ -30,10 +32,14 @@
private static final Logger log = LoggerFactory.getLogger(SshPluginStarterCallback.class);
private final DispatchCommandProvider root;
+ private final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
@Inject
- SshPluginStarterCallback(@CommandName(Commands.ROOT) DispatchCommandProvider root) {
+ SshPluginStarterCallback(
+ @CommandName(Commands.ROOT) DispatchCommandProvider root,
+ DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
this.root = root;
+ this.dynamicBeans = dynamicBeans;
}
@Override
@@ -58,10 +64,19 @@
try {
return plugin.getSshInjector().getProvider(key);
} catch (RuntimeException err) {
- log.warn(
- String.format("Plugin %s did not define its top-level command", plugin.getName()), err);
+ if (!providesDynamicOptions(plugin)) {
+ log.warn(
+ String.format(
+ "Plugin %s did not define its top-level command nor any DynamicOptions",
+ plugin.getName()),
+ err);
+ }
}
}
return null;
}
+
+ private boolean providesDynamicOptions(Plugin plugin) {
+ return dynamicBeans.plugins().contains(plugin.getName());
+ }
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
index 1ed0bb0..0c8c74a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -82,13 +82,7 @@
@Override
public void start(final Environment env) {
- startThread(
- new Runnable() {
- @Override
- public void run() {
- runImp();
- }
- });
+ startThread(this::runImp);
}
private void runImp() {
diff --git a/gerrit-test-util/src/main/java/com/google/gerrit/extensions/restapi/BinaryResultSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/restapi/BinaryResultSubject.java
index 989ab0f..30ac496 100644
--- a/gerrit-test-util/src/main/java/com/google/gerrit/extensions/restapi/BinaryResultSubject.java
+++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/restapi/BinaryResultSubject.java
@@ -18,6 +18,7 @@
import com.google.common.truth.FailureStrategy;
import com.google.common.truth.PrimitiveByteArraySubject;
+import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
import com.google.common.truth.SubjectFactory;
import com.google.common.truth.Truth;
@@ -51,6 +52,15 @@
super(failureStrategy, binaryResult);
}
+ public StringSubject asString() throws IOException {
+ isNotNull();
+ // We shouldn't close the BinaryResult within this method as it might still
+ // be used afterwards. Besides, closing it doesn't have an effect for most
+ // implementations of a BinaryResult.
+ BinaryResult binaryResult = actual();
+ return Truth.assertThat(binaryResult.asString());
+ }
+
public PrimitiveByteArraySubject bytes() throws IOException {
isNotNull();
// We shouldn't close the BinaryResult within this method as it might still
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index 11f380d..8aa5b9e 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -44,6 +44,8 @@
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -58,8 +60,10 @@
import org.kohsuke.args4j.spi.BooleanOptionHandler;
import org.kohsuke.args4j.spi.EnumOptionHandler;
import org.kohsuke.args4j.spi.FieldSetter;
+import org.kohsuke.args4j.spi.MethodSetter;
import org.kohsuke.args4j.spi.OptionHandler;
import org.kohsuke.args4j.spi.Setter;
+import org.kohsuke.args4j.spi.Setters;
/**
* Extended command line parser which handles --foo=value arguments.
@@ -253,6 +257,10 @@
return findHandler(makeOption(name)) instanceof BooleanOptionHandler;
}
+ public void parseWithPrefix(String prefix, Object bean) {
+ parser.parseWithPrefix(prefix, bean);
+ }
+
private String makeOption(String name) {
if (!name.startsWith("-")) {
if (name.length() == 1) {
@@ -313,6 +321,70 @@
throw new CmdLineException(parser, String.format("invalid boolean \"%s=%s\"", name, value));
}
+ private static class PrefixedOption implements Option {
+ String prefix;
+ Option o;
+
+ PrefixedOption(String prefix, Option o) {
+ this.prefix = prefix;
+ this.o = o;
+ }
+
+ @Override
+ public String name() {
+ return getPrefixedName(prefix, o.name());
+ }
+
+ @Override
+ public String[] aliases() {
+ String[] prefixedAliases = new String[o.aliases().length];
+ for (int i = 0; i < prefixedAliases.length; i++) {
+ prefixedAliases[i] = getPrefixedName(prefix, o.aliases()[i]);
+ }
+ return prefixedAliases;
+ }
+
+ @Override
+ public String usage() {
+ return o.usage();
+ }
+
+ @Override
+ public String metaVar() {
+ return o.metaVar();
+ }
+
+ @Override
+ public boolean required() {
+ return o.required();
+ }
+
+ @Override
+ public boolean hidden() {
+ return o.hidden();
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public Class<? extends OptionHandler> handler() {
+ return o.handler();
+ }
+
+ @Override
+ public String[] depends() {
+ return o.depends();
+ }
+
+ @Override
+ public Class<? extends Annotation> annotationType() {
+ return o.annotationType();
+ }
+
+ private static String getPrefixedName(String prefix, String name) {
+ return "--" + prefix + name;
+ }
+ }
+
private class MyParser extends org.kohsuke.args4j.CmdLineParser {
@SuppressWarnings("rawtypes")
private List<OptionHandler> optionsList;
@@ -324,6 +396,25 @@
ensureOptionsInitialized();
}
+ // NOTE: Argument annotations on bean are ignored.
+ public void parseWithPrefix(String prefix, Object bean) {
+ // recursively process all the methods/fields.
+ for (Class<?> c = bean.getClass(); c != null; c = c.getSuperclass()) {
+ for (Method m : c.getDeclaredMethods()) {
+ Option o = m.getAnnotation(Option.class);
+ if (o != null) {
+ addOption(new MethodSetter(this, bean, m), new PrefixedOption(prefix, o));
+ }
+ }
+ for (Field f : c.getDeclaredFields()) {
+ Option o = f.getAnnotation(Option.class);
+ if (o != null) {
+ addOption(Setters.create(f, bean), new PrefixedOption(prefix, o));
+ }
+ }
+ }
+ }
+
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
protected OptionHandler createOptionHandler(final OptionDef option, final Setter setter) {
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index acc9b86..af269f1 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-war</artifactId>
- <version>2.14-SNAPSHOT</version>
+ <version>2.15-SNAPSHOT</version>
<packaging>war</packaging>
<name>Gerrit Code Review - WAR</name>
<description>Gerrit WAR</description>
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index 502c060..7b5acd8 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,12 +1,12 @@
load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_LOCAL", "MAVEN_CENTRAL", "maven_jar")
-_JGIT_VERS = "4.7.0.201704051617-r"
+_JGIT_VERS = "4.7.0.201704051617-r.15-gc4e952109"
-_DOC_VERS = _JGIT_VERS # Set to _JGIT_VERS unless using a snapshot
+_DOC_VERS = "4.7.0.201704051617-r" # Set to _JGIT_VERS unless using a snapshot
JGIT_DOC_URL = "http://download.eclipse.org/jgit/site/" + _DOC_VERS + "/apidocs"
-_JGIT_REPO = MAVEN_CENTRAL # Leave here even if set to MAVEN_CENTRAL.
+_JGIT_REPO = GERRIT # Leave here even if set to MAVEN_CENTRAL.
# set this to use a local version.
# "/home/<user>/projects/jgit"
@@ -26,28 +26,28 @@
name = "jgit_lib",
artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
repository = _JGIT_REPO,
- sha1 = "99be65d1827276b97d4f51668b60f4a38f282bda",
- src_sha1 = "de519d6f352aaf12e4c65f7590591326ac24d2e8",
+ sha1 = "875277521153030e2bdab12bf602b740232b2b28",
+ src_sha1 = "f9adcd3ef0f77c5db16569771f95bc0142c36f46",
unsign = True,
)
maven_jar(
name = "jgit_servlet",
artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
repository = _JGIT_REPO,
- sha1 = "72fa98ebf001aadd3dcb99ca8f7fcd90983da56b",
+ sha1 = "e1037f50696a6e19fb5d30f9d44cb31e3c5fe8b0",
unsign = True,
)
maven_jar(
name = "jgit_archive",
artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
repository = _JGIT_REPO,
- sha1 = "f825504a903dfe8d3daa61d6ab5c26fbad92c954",
+ sha1 = "660bc82c9ff3c33249d269860d9793e830d6c374",
)
maven_jar(
name = "jgit_junit",
artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
repository = _JGIT_REPO,
- sha1 = "e0dbc6d3568b2ba65c9421af2f06e4158a624bcb",
+ sha1 = "c2b28646cc2531df947a9e0f73fa9c415567b05e",
unsign = True,
)
diff --git a/plugins/replication b/plugins/replication
index 47c2691..032d195 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 47c2691a109ffedf8737f3697a80eef82dd5a4a9
+Subproject commit 032d195285b11213821ed695ecadce51238d9bb8
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 76979f1..864e294 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -94,6 +94,9 @@
./polygerrit-ui/app/run_test.sh
```
+To allow the tests to run in Safari it is necessary to enable the
+"Allow Remote Automation" option under the "Develop" menu.
+
If you need to pass additional arguments to `wct`:
```sh
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
index 68656436..ad76f10 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -25,6 +25,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-list-item.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-change-list-item></gr-change-list-item>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
index 59edf4c..0097a50 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -25,6 +25,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-list-view.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-change-list-view></gr-change-list-view>
@@ -50,8 +52,11 @@
sandbox = sinon.sandbox.create();
});
- teardown(function() {
- sandbox.restore();
+ teardown(function(done) {
+ flush(function() {
+ sandbox.restore();
+ done();
+ });
});
test('url is properly encoded', function() {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index ce27dab..1a1abff 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -25,6 +25,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-list.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-change-list></gr-change-list>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index e8ca5bf..977552a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -67,7 +67,7 @@
this._loading = false;
}.bind(this)).catch(function(err) {
this._loading = false;
- console.error(err.message);
+ console.warn(err.message);
}.bind(this));
},
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
index 6be8f7b..718e59c 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -23,6 +23,8 @@
<link rel="import" href="gr-dashboard-view.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-dashboard-view></gr-dashboard-view>
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
index 46f2ed2..9a0b115 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
@@ -22,7 +22,6 @@
*
* @event add
*/
-
properties: {
borderless: Boolean,
change: Object,
@@ -64,6 +63,10 @@
this.$.input.clear();
},
+ setText: function(text) {
+ this.$.input.setText(text);
+ },
+
_handleInputCommit: function(e) {
this.fire('add', {value: e.detail.value});
},
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
index 71e84d0..8797d32 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
@@ -25,6 +25,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-account-entry.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-account-entry></gr-account-entry>
@@ -169,5 +171,15 @@
});
});
});
+
+ test('setText', function() {
+ // Spy on query, as that is called when _updateSuggestions proceeds.
+ var suggestSpy = sandbox.spy(element.$.input, 'query');
+ element.setText('test text');
+ flushAsynchronousOperations();
+
+ assert.equal(element.$.input.$.input.value, 'test text');
+ assert.isFalse(suggestSpy.called);
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
index 35311f9..bf063b3 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
@@ -14,9 +14,17 @@
(function() {
'use strict';
+ var VALID_EMAIL_ALERT = 'Please input a valid email.';
+
Polymer({
is: 'gr-account-list',
+ /**
+ * Fired when user inputs an invalid email address.
+ *
+ * @event show-alert
+ */
+
properties: {
accounts: {
type: Array,
@@ -35,6 +43,7 @@
type: Boolean,
value: false,
},
+
/**
* When true, the account-entry autocomplete uses the account suggest API
* endpoint, which suggests any account in that Gerrit instance (and does
@@ -48,6 +57,15 @@
type: Boolean,
value: false,
},
+
+ /**
+ * When true, allows for non-suggested inputs to be added.
+ */
+ allowAnyInput: {
+ type: Boolean,
+ value: false,
+ },
+
/**
* Array of values (groups/accounts) that are removable. When this prop is
* undefined, all values are removable.
@@ -88,6 +106,17 @@
var group = Object.assign({}, reviewer.group,
{_pendingAdd: true, _group: true});
this.push('accounts', group);
+ } else if (this.allowAnyInput) {
+ if (reviewer.indexOf('@') === -1) {
+ // Repopulate the input with what the user tried to enter and have
+ // a toast tell them why they can't enter it.
+ this.$.entry.setText(reviewer);
+ this.dispatchEvent(new CustomEvent('show-alert',
+ {detail: {message: VALID_EMAIL_ALERT}, bubbles: true}));
+ } else {
+ var account = {email: reviewer, _pendingAdd: true};
+ this.push('accounts', account);
+ }
}
this.pendingConfirmation = null;
},
@@ -110,11 +139,23 @@
return classes.join(' ');
},
+ _accountMatches: function(a, b) {
+ if (a && b) {
+ if (a._account_id) {
+ return a._account_id === b._account_id;
+ }
+ if (a.email) {
+ return a.email === b.email;
+ }
+ }
+ return a === b;
+ },
+
_computeRemovable: function(account) {
if (this.readonly) { return false; }
if (this.removableValues) {
for (var i = 0; i < this.removableValues.length; i++) {
- if (this.removableValues[i]._account_id === account._account_id) {
+ if (this._accountMatches(this.removableValues[i], account)) {
return true;
}
}
@@ -137,7 +178,7 @@
if (toRemove._group) {
matches = toRemove.id === account.id;
} else {
- matches = toRemove._account_id === account._account_id;
+ matches = this._accountMatches(toRemove, account);
}
if (matches) {
this.splice('accounts', i, 1);
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
index a334b03..cb4a845 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
@@ -24,6 +24,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-account-list.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-account-list></gr-account-list>
@@ -43,6 +45,7 @@
var groupId = 'group' + (++_nextAccountId);
return {
id: groupId,
+ _group: true,
};
};
@@ -264,6 +267,44 @@
assert.isTrue(element.$.entry.hasAttribute('hidden'));
});
+ suite('allowAnyInput', function() {
+ var entry;
+
+ setup(function() {
+ entry = element.$.entry;
+ sandbox.stub(entry, '_getReviewerSuggestions');
+ sandbox.stub(entry.$.input, '_updateSuggestions');
+ element.allowAnyInput = true;
+ });
+
+ test('adds emails', function() {
+ var accountLen = element.accounts.length;
+ element._handleAdd({detail: {value: 'test@test'}});
+ assert.equal(element.accounts.length, accountLen + 1);
+ assert.equal(element.accounts[accountLen].email, 'test@test');
+ });
+
+ test('toasts on invalid email', function() {
+ var toastHandler = sandbox.stub();
+ element.addEventListener('show-alert', toastHandler);
+ element._handleAdd({detail: {value: 'test'}});
+ assert.isTrue(toastHandler.called);
+ });
+ });
+
+ test('_accountMatches', function() {
+ var acct = makeAccount();
+
+ assert.isTrue(element._accountMatches(acct, acct));
+ acct.email = 'test';
+ assert.isTrue(element._accountMatches(acct, acct));
+ assert.isTrue(element._accountMatches({email: 'test'}, acct));
+
+ assert.isFalse(element._accountMatches({}, acct));
+ assert.isFalse(element._accountMatches({email: 'test2'}, acct));
+ assert.isFalse(element._accountMatches({_account_id: -1}, acct));
+ });
+
suite('keyboard interactions', function() {
test('backspace at text input start removes last account', function() {
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 c0293ad..83a1c7e 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
@@ -25,6 +25,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-actions.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-change-actions></gr-change-actions>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 932b914..39c6a1f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -16,6 +16,7 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
<link rel="import" href="../../shared/gr-label/gr-label.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
@@ -74,6 +75,18 @@
.webLink {
display: block;
}
+ section.assignee {
+ @apply(--change-metadata-assignee);
+ }
+ section.labelStatus {
+ @apply(--change-metadata-label-status);
+ }
+ section.strategy {
+ @apply(--change-metadata-strategy);
+ }
+ section.topic {
+ @apply(--change-metadata-topic);
+ }
@media screen and (max-width: 50em), screen and (min-width: 75em) {
:host {
display: table;
@@ -167,11 +180,15 @@
</template>
<section>
<span class="title">Project</span>
- <span class="value">[[change.project]]</span>
+ <span class="value">
+ <a href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a>
+ </span>
</section>
<section>
<span class="title">Branch</span>
- <span class="value">[[change.branch]]</span>
+ <span class="value">
+ <a href$="[[_computeBranchURL(change.project, change.branch)]]">[[change.branch]]</a>
+ </span>
</section>
<section>
<span class="title">Topic</span>
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 e4521c3..56394fd 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
@@ -48,6 +48,7 @@
behaviors: [
Gerrit.RESTClientBehavior,
+ Gerrit.URLEncodingBehavior,
],
observers: [
@@ -225,6 +226,22 @@
return output;
},
+ _computeProjectURL: function(project) {
+ return '/q/status:open+project:' + this.encodeURL(project, false);
+ },
+
+ _computeBranchURL: function(project, branch) {
+ var status;
+ if (this.change.status == this.ChangeStatus.NEW) {
+ status = 'open';
+ } else {
+ status = this.change.status.toLowerCase();
+ }
+ return '/q/project:' + this.encodeURL(project, false) +
+ ' branch:' + this.encodeURL(branch, false) +
+ ' status:' + this.encodeURL(status, false);
+ },
+
_computeTopicHref: function(topic) {
var encodedTopic = encodeURIComponent('\"' + topic + '\"');
return '/q/topic:' + encodeURIComponent(encodedTopic) +
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 10497c0..4eda281 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
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-metadata</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-metadata.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-change-metadata></gr-change-metadata>
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 48ea91c..664d44e 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
@@ -384,6 +384,7 @@
change="[[_change]]"
has-parent="{{hasParent}}"
loading="{{_relatedChangesLoading}}"
+ on-update="_updateRelatedChangeMaxHeight"
patch-num="[[_computeLatestPatchNum(_allPatchSets)]]">
</gr-related-changes-list>
<div
@@ -485,6 +486,7 @@
<gr-overlay id="replyOverlay"
class="scrollable"
no-cancel-on-outside-click
+ no-cancel-on-esc-key
on-iron-overlay-opened="_handleReplyOverlayOpen"
with-backdrop>
<gr-reply-dialog id="replyDialog"
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 1420949..0dd9e39 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,12 @@
* @event page-error
*/
+ /**
+ * Fired if being logged in is required.
+ *
+ * @event show-auth-required
+ */
+
properties: {
/**
* URL params passed from the router.
@@ -718,11 +724,18 @@
_handleAKey: function(e) {
if (this.shouldSuppressKeyboardShortcut(e) ||
- this.modifierPressed(e) ||
- !this._loggedIn) { return; }
+ this.modifierPressed(e)) {
+ return;
+ }
+ this._getLoggedIn().then(function(isLoggedIn) {
+ if (!isLoggedIn) {
+ this.fire('show-auth-required');
+ return;
+ }
- e.preventDefault();
- this._openReplyDialog();
+ e.preventDefault();
+ this._openReplyDialog();
+ }.bind(this));
},
_handleDKey: function(e) {
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 907417c..3bdfa2f 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
@@ -18,13 +18,15 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-view</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/page/page.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-view.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-change-view></gr-change-view>
@@ -48,8 +50,11 @@
element = fixture('basic');
});
- teardown(function() {
- sandbox.restore();
+ teardown(function(done) {
+ flush(function() {
+ sandbox.restore();
+ done();
+ });
});
suite('keyboard shortcuts', function() {
@@ -70,19 +75,36 @@
assert(showStub.lastCall.calledWithExactly('/dashboard/self'));
});
- test('A should toggle overlay', function() {
+ test('A fires an error event when not logged in', function(done) {
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(false));
+ var loggedInErrorSpy = sandbox.spy();
+ element.addEventListener('show-auth-required', loggedInErrorSpy);
MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- var overlayEl = element.$.replyOverlay;
- assert.isFalse(overlayEl.opened);
- element._loggedIn = true;
+ flush(function() {
+ assert.isFalse(element.$.replyOverlay.opened);
+ assert.isTrue(loggedInErrorSpy.called);
+ done();
+ });
+ });
+ test('shift A does not open reply overlay', function(done) {
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a');
- assert.isFalse(overlayEl.opened);
+ flush(function() {
+ assert.isFalse(element.$.replyOverlay.opened);
+ done();
+ });
+ });
+ test('A toggles overlay when logged in', function(done) {
+ sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
- assert.isTrue(overlayEl.opened);
- overlayEl.close();
- assert.isFalse(overlayEl.opened);
+ flush(function() {
+ assert.isTrue(element.$.replyOverlay.opened);
+ element.$.replyOverlay.close();
+ assert.isFalse(element.$.replyOverlay.opened);
+ done();
+ });
});
test('X should expand all messages', function() {
@@ -795,16 +817,16 @@
test('reply dialog focus can be controlled', function() {
var FocusTarget = element.$.replyDialog.FocusTarget;
- var openSpy = sandbox.spy(element, '_openReplyDialog');
+ var openStub = sandbox.stub(element, '_openReplyDialog');
var e = {detail: {}};
element._handleShowReplyDialog(e);
- assert(openSpy.lastCall.calledWithExactly(FocusTarget.REVIEWERS),
+ assert(openStub.lastCall.calledWithExactly(FocusTarget.REVIEWERS),
'_openReplyDialog should have been passed REVIEWERS');
e.detail.value = {ccsOnly: true};
element._handleShowReplyDialog(e);
- assert(openSpy.lastCall.calledWithExactly(FocusTarget.CCS),
+ assert(openStub.lastCall.calledWithExactly(FocusTarget.CCS),
'_openReplyDialog should have been passed CCS');
});
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index d65cc9f..34f2951 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -18,10 +18,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-comment-list</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="gr-comment-list.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-comment-list></gr-comment-list>
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
index 36b1628..c8faff5 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-commit-info</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-commit-info.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-commit-info></gr-commit-info>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
index a63388e..3c1cf2b 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-cherrypick-dialog</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-confirm-cherrypick-dialog.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-confirm-cherrypick-dialog></gr-confirm-cherrypick-dialog>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
index f48fe36..37eb812 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-rebase-dialog</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-confirm-rebase-dialog.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-confirm-rebase-dialog></gr-confirm-rebase-dialog>
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 f5672d3..d5c459b 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
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-revert-dialog</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-confirm-revert-dialog.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-confirm-revert-dialog></gr-confirm-revert-dialog>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
index ce28d4e..0635d6d 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -24,6 +24,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-download-dialog.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-download-dialog></gr-download-dialog>
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 0698a0c..a967da2 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
@@ -232,95 +232,103 @@
</label>
</div>
</header>
- <template is="dom-repeat"
- items="[[_shownFiles]]"
- as="file"
- initial-count="[[_fileListIncrement]]">
- <div class="file-row row">
- <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
- <input type="checkbox" checked="[[file.isReviewed]]"
- data-path$="[[file.__path]]" on-change="_handleReviewedChange"
- class="reviewed" aria-label="Reviewed checkbox">
- </div>
- <div class$="[[_computeClass('status', file.__path)]]"
- tabindex="0"
- aria-label$="[[_computeFileStatusLabel(file.status)]]">
- [[_computeFileStatus(file.status)]]
- </div>
- <a class$="[[_computePathClass(file.__path, _expandedFilePaths.*)]]"
- href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]"
- on-tap="_handleFileTap">
- <div title$="[[_computeFileDisplayName(file.__path)]]"
- class="fullFileName">
- [[_computeFileDisplayName(file.__path)]]
- </div>
- <div title$="[[_computeFileDisplayName(file.__path)]]"
- class="truncatedFileName">
- [[_computeTruncatedFileDisplayName(file.__path)]]
- </div>
- <div class="oldPath" hidden$="[[!file.old_path]]" hidden
- title$="[[file.old_path]]">
- [[file.old_path]]
- </div>
- </a>
- <div class="comments desktop">
- <span class="drafts">
- [[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]]
- </span>
- [[_computeCommentsString(comments, patchRange.patchNum, file.__path)]]
- [[_computeUnresolvedString(comments, drafts, patchRange.patchNum, file.__path)]]
- </div>
- <div class="comments mobile">
- <span class="drafts">
- [[_computeDraftsStringMobile(drafts, patchRange.patchNum,
- file.__path)]]
- </span>
- [[_computeCommentsStringMobile(comments, patchRange.patchNum,
- file.__path)]]
- </div>
- <div class$="[[_computeClass('stats', file.__path)]]">
- <span
- class="added"
- tabindex="0"
- aria-label$="[[file.lines_inserted]] lines added"
- hidden$=[[file.binary]]>
- +[[file.lines_inserted]]
- </span>
- <span
- class="removed"
- tabindex="0"
- aria-label$="[[file.lines_deleted]] lines removed"
- hidden$=[[file.binary]]>
- -[[file.lines_deleted]]
- </span>
- <span class$="[[_computeBinaryClass(file.size_delta)]]"
- hidden$=[[!file.binary]]>
- [[_formatBytes(file.size_delta)]]
- [[_formatPercentage(file.size, file.size_delta)]]
- </span>
- </div>
- <div class="show-hide" hidden$="[[_userPrefs.expand_inline_diffs]]">
- <label class="show-hide">
- <input type="checkbox" class="show-hide"
- checked$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
+ <div on-tap="_handleFileListTap">
+ <template is="dom-repeat"
+ items="[[_shownFiles]]"
+ id="files"
+ as="file"
+ initial-count="[[_fileListIncrement]]"
+ target-framerate="1">
+ <div class="file-row row">
+ <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
+ <input type="checkbox" checked="[[file.isReviewed]]"
data-path$="[[file.__path]]"
- on-change="_handleHiddenChange">
- [[_computeShowHideText(file.__path, _expandedFilePaths.*)]]
- </label>
+ class="reviewed" aria-label="Reviewed checkbox">
+ </div>
+ <div class$="[[_computeClass('status', file.__path)]]"
+ tabindex="0"
+ aria-label$="[[_computeFileStatusLabel(file.status)]]">
+ [[_computeFileStatus(file.status)]]
+ </div>
+ <a class$="[[_computePathClass(file.__path, _expandedFilePaths.*)]]"
+ href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]"
+ data-path$="[[file.__path]]">
+ <div title$="[[_computeFileDisplayName(file.__path)]]"
+ class="fullFileName">
+ [[_computeFileDisplayName(file.__path)]]
+ </div>
+ <div title$="[[_computeFileDisplayName(file.__path)]]"
+ class="truncatedFileName">
+ [[_computeTruncatedFileDisplayName(file.__path)]]
+ </div>
+ <div class="oldPath" hidden$="[[!file.old_path]]" hidden
+ title$="[[file.old_path]]">
+ [[file.old_path]]
+ </div>
+ </a>
+ <div class="comments desktop">
+ <span class="drafts">
+ [[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]]
+ </span>
+ [[_computeCommentsString(comments, patchRange.patchNum, file.__path)]]
+ [[_computeUnresolvedString(comments, drafts, patchRange.patchNum, file.__path)]]
+ </div>
+ <div class="comments mobile">
+ <span class="drafts">
+ [[_computeDraftsStringMobile(drafts, patchRange.patchNum,
+ file.__path)]]
+ </span>
+ [[_computeCommentsStringMobile(comments, patchRange.patchNum,
+ file.__path)]]
+ </div>
+ <div class$="[[_computeClass('stats', file.__path)]]">
+ <span
+ class="added"
+ tabindex="0"
+ aria-label$="[[file.lines_inserted]] lines added"
+ hidden$=[[file.binary]]>
+ +[[file.lines_inserted]]
+ </span>
+ <span
+ class="removed"
+ tabindex="0"
+ aria-label$="[[file.lines_deleted]] lines removed"
+ hidden$=[[file.binary]]>
+ -[[file.lines_deleted]]
+ </span>
+ <span class$="[[_computeBinaryClass(file.size_delta)]]"
+ hidden$=[[!file.binary]]>
+ [[_formatBytes(file.size_delta)]]
+ [[_formatPercentage(file.size, file.size_delta)]]
+ </span>
+ </div>
+ <div class="show-hide" hidden$="[[_userPrefs.expand_inline_diffs]]">
+ <label class="show-hide" data-path$="[[file.__path]]"
+ data-expand=true>
+ <input type="checkbox" class="show-hide"
+ checked$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
+ data-path$="[[file.__path]]" data-expand=true>
+ [[_computeShowHideText(file.__path, _expandedFilePaths.*)]]
+ </label>
+ </div>
</div>
- </div>
- <gr-diff
- no-auto-render
- hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
- project="[[change.project]]"
- commit="[[change.current_revision]]"
- change-num="[[changeNum]]"
- patch-range="[[patchRange]]"
- path="[[file.__path]]"
- prefs="[[_diffPrefs]]"
- project-config="[[projectConfig]]"
- view-mode="[[_getDiffViewMode(diffViewMode, _userPrefs)]]"></gr-diff>
- </template>
+ <template is="dom-if"
+ if="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]">
+ <gr-diff
+ no-auto-render
+ inline-index=[[index]]
+ hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]"
+ project="[[change.project]]"
+ commit="[[change.current_revision]]"
+ change-num="[[changeNum]]"
+ patch-range="[[patchRange]]"
+ path="[[file.__path]]"
+ prefs="[[_diffPrefs]]"
+ project-config="[[projectConfig]]"
+ view-mode="[[_getDiffViewMode(diffViewMode, _userPrefs)]]"></gr-diff>
+ </template>
+ </template>
+ </div>
<div class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]">
<div class="total-stats" hidden$="[[_hideChangeTotals]]">
<span
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 a0189f2..cbba46e 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
@@ -226,10 +226,6 @@
return parseInt(patchNum, 10) >= parseInt(currentPatchNum, 10);
},
- _handleHiddenChange: function(e) {
- this._togglePathExpanded(e.model.file.__path);
- },
-
_togglePathExpanded: function(path) {
// Is the path in the list of expanded diffs? IF so remove it, otherwise
// add it to the list.
@@ -400,15 +396,29 @@
});
},
- _handleFileTap: function(e) {
+ /**
+ * Handle all events from the file list dom-repeat so event handleers don't
+ * have to get registered for potentially very long lists.
+ */
+ _handleFileListTap: function(e) {
+ // Handle checkbox mark as reviewed.
+ if (e.target.classList.contains('reviewed')) {
+ return this._handleReviewedChange(e);
+ }
+
+ // Check to see if the file should be expanded.
+ var path = e.target.dataset.path || e.target.parentElement.dataset.path;
+
// If the user prefers to expand inline diffs rather than opening the diff
// view, intercept the click event.
- if (e.detail.sourceEvent.metaKey || e.detail.sourceEvent.ctrlKey) {
+ if (!path || e.detail.sourceEvent.metaKey ||
+ e.detail.sourceEvent.ctrlKey) {
return;
}
- if (this._userPrefs && this._userPrefs.expand_inline_diffs) {
+ if (e.target.dataset.expand ||
+ this._userPrefs && this._userPrefs.expand_inline_diffs) {
e.preventDefault();
- this._handleHiddenChange(e);
+ this._togglePathExpanded(path);
}
},
@@ -445,7 +455,10 @@
},
_handleDownKey: function(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
+
e.preventDefault();
if (this._showInlineDiffs) {
this.$.diffCursor.moveDown();
@@ -456,7 +469,9 @@
},
_handleUpKey: function(e) {
- if (this.shouldSuppressKeyboardShortcut(e)) { return; }
+ if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) {
+ return;
+ }
e.preventDefault();
if (this._showInlineDiffs) {
@@ -775,6 +790,9 @@
var timerName = 'Expand ' + newPaths.length + ' diffs';
this.$.reporting.time(timerName);
+ // Required so that the newly created diff view is included in this.diffs.
+ Polymer.dom.flush();
+
this._renderInOrder(newPaths, this.diffs, newPaths.length)
.then(function() {
this.$.reporting.timeEnd(timerName);
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 66ad66c..6197541 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
@@ -18,7 +18,7 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-file-list</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/page/page.js"></script>
<script src="../../../scripts/util.js"></script>
@@ -26,6 +26,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-file-list.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-file-list></gr-file-list>
@@ -306,6 +308,9 @@
assert.isTrue(items[0].classList.contains('selected'));
assert.isFalse(items[1].classList.contains('selected'));
assert.isFalse(items[2].classList.contains('selected'));
+ // j with a modifier should not move the cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 74, 'shift', 'j');
+ assert.equal(element.$.fileCursor.index, 0);
MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j');
assert.equal(element.$.fileCursor.index, 1);
assert.equal(element.selectedIndex, 1);
@@ -317,6 +322,9 @@
MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter');
assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'),
'Should navigate to /c/42/2/myfile.txt');
+ // k with a modifier should not move the cursor.
+ MockInteractions.pressAndReleaseKeyOn(element, 75, 'shift', 'k');
+ assert.equal(element.$.fileCursor.index, 2);
MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k');
assert.equal(element.$.fileCursor.index, 1);
@@ -335,7 +343,8 @@
test('i key shows/hides selected inline diff', function() {
sandbox.stub(element, '_expandedPathsChanged');
flushAsynchronousOperations();
- element.$.fileCursor.stops = element.diffs;
+ var files = Polymer.dom(element.root).querySelectorAll('.file-row');
+ element.$.fileCursor.stops = files;
element.$.fileCursor.setCursorAtIndex(0);
MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i');
flushAsynchronousOperations();
@@ -635,10 +644,13 @@
flushAsynchronousOperations();
var fileRows =
Polymer.dom(element.root).querySelectorAll('.row:not(.header)');
+ // Because the label surrounds the input, the tap event is triggered
+ // there first.
+ var showHideLabel = fileRows[0].querySelector('label.show-hide');
var showHideCheck = fileRows[0].querySelector(
'input.show-hide[type="checkbox"]');
assert.isNotOk(showHideCheck.checked);
- MockInteractions.tap(showHideCheck);
+ MockInteractions.tap(showHideLabel);
assert.isOk(showHideCheck.checked);
assert.notEqual(element._expandedFilePaths.indexOf('myfile.txt'), -1);
});
@@ -672,6 +684,13 @@
};
element.$.fileCursor.setCursorAtIndex(0);
flushAsynchronousOperations();
+
+ // Tap on a file to generate the diff.
+ var row = Polymer.dom(element.root)
+ .querySelectorAll('.row:not(.header) label.show-hide')[0];
+
+ MockInteractions.tap(row);
+ flushAsynchronousOperations();
var diffDisplay = element.diffs[0];
element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'};
assert.equal(element.diffViewMode, 'SIDE_BY_SIDE');
@@ -761,18 +780,18 @@
// Remove href attribute so the app doesn't route to a diff view
commitMsgFile.removeAttribute('href');
- var hiddenChangeSpy = sandbox.spy(element, '_handleHiddenChange');
+ var togglePathSpy = sandbox.spy(element, '_togglePathExpanded');
MockInteractions.tap(commitMsgFile);
flushAsynchronousOperations();
- assert(hiddenChangeSpy.notCalled, 'file is opened as diff view');
+ assert(togglePathSpy.notCalled, 'file is opened as diff view');
assert.isNotOk(element.$$('.expanded'));
element._userPrefs = {expand_inline_diffs: true};
flushAsynchronousOperations();
MockInteractions.tap(commitMsgFile);
flushAsynchronousOperations();
- assert(hiddenChangeSpy.calledOnce, 'file is expanded');
+ assert(togglePathSpy.calledOnce, 'file is expanded');
assert.isOk(element.$$('.expanded'));
});
@@ -810,32 +829,37 @@
element.push('_expandedFilePaths', path);
});
- suite('_handleFileTap', function() {
+ suite('_handleFileListTap', function() {
function testForModifier(modifier) {
var e = {preventDefault: function() {}};
e.detail = {sourceEvent: {}};
+ e.target = {
+ dataset: {path: '/test'},
+ classList: element.classList,
+ };
+
e.detail.sourceEvent[modifier] = true;
- var hiddenChangeStub = sandbox.stub(element, '_handleHiddenChange');
+ var togglePathStub = sandbox.stub(element, '_togglePathExpanded');
element._userPrefs = { expand_inline_diffs: true };
- element._handleFileTap(e);
- assert.isFalse(hiddenChangeStub.called);
+ element._handleFileListTap(e);
+ assert.isFalse(togglePathStub.called);
e.detail.sourceEvent[modifier] = false;
- element._handleFileTap(e);
- assert.equal(hiddenChangeStub.callCount, 1);
+ element._handleFileListTap(e);
+ assert.equal(togglePathStub.callCount, 1);
element._userPrefs = { expand_inline_diffs: false };
- element._handleFileTap(e);
- assert.equal(hiddenChangeStub.callCount, 1);
+ element._handleFileListTap(e);
+ assert.equal(togglePathStub.callCount, 1);
}
- test('_handleFileTap meta', function() {
+ test('_handleFileListTap meta', function() {
testForModifier('metaKey');
});
- test('_handleFileTap ctrl', function() {
+ test('_handleFileListTap ctrl', function() {
testForModifier('ctrlKey');
});
});
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
index 831914e..e9ae55d 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -82,7 +82,7 @@
overflow: hidden;
text-overflow: ellipsis;
}
- .collapsed .name,
+ .collapsed .author,
.collapsed .content,
.collapsed .message,
.collapsed .updateCategory,
@@ -108,11 +108,11 @@
.collapsed .date {
position: static;
}
- .collapsed .name {
+ .collapsed .author {
color: var(--default-text-color);
margin-right: .4em;
}
- .expanded .name {
+ .expanded .author {
cursor: pointer;
}
.date {
@@ -128,7 +128,13 @@
<div class$="[[_computeClass(_expanded, showAvatar)]]">
<gr-avatar account="[[author]]" image-size="100"></gr-avatar>
<div class="contentContainer">
- <div class="name" on-tap="_handleNameTap">[[author.name]]</div>
+ <div class="author" on-tap="_handleAuthorTap">
+ <span hidden$="[[!showOnBehalfOf]]">
+ <span class="name">[[message.real_author.name]]</span>
+ on behalf of
+ </span>
+ <span class="name">[[author.name]]</span>
+ </div>
<template is="dom-if" if="[[message.message]]">
<div class="content">
<div class="message hideOnOpen">[[message.message]]</div>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index e782943..5467af9 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -62,6 +62,10 @@
type: Boolean,
computed: '_computeShowAvatar(author, config)',
},
+ showOnBehalfOf: {
+ type: Boolean,
+ computed: '_computeShowOnBehalfOf(message)',
+ },
showReplyButton: {
type: Boolean,
computed: '_computeShowReplyButton(message, _loggedIn)',
@@ -107,6 +111,12 @@
return !!(author && config && config.plugin && config.plugin.has_avatars);
},
+ _computeShowOnBehalfOf: function(message) {
+ var author = message.author || message.updated_by;
+ return !!(author && message.real_author &&
+ author._account_id != message.real_author._account_id);
+ },
+
_computeShowReplyButton: function(message, loggedIn) {
return !!message.message && loggedIn;
},
@@ -132,7 +142,7 @@
this.set('message.expanded', true);
},
- _handleNameTap: function(e) {
+ _handleAuthorTap: function(e) {
if (!this.message.expanded) { return; }
e.stopPropagation();
this.set('message.expanded', false);
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 0f6c85d..72c09d9 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -24,6 +24,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-message.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-message></gr-message>
@@ -90,7 +92,7 @@
var content = element.$$('.contentContainer');
assert.isOk(content);
assert.strictEqual(element.$$('gr-account-chip').account, reviewer);
- assert.equal(author.name, element.$$('.name').textContent);
+ assert.equal(author.name, element.$$('.author > .name').textContent);
});
test('autogenerated prefix hiding', function() {
@@ -143,5 +145,23 @@
assert.isFalse(element._computeShowReplyButton(message, false));
assert.isTrue(element._computeShowReplyButton(message, true));
});
+
+ test('_computeShowOnBehalfOf', function() {
+ var message = {
+ message: '...',
+ };
+ assert.isNotOk(element._computeShowOnBehalfOf(message));
+ message.author = {_account_id: 1115495};
+ assert.isNotOk(element._computeShowOnBehalfOf(message));
+ message.real_author = {_account_id: 1115495};
+ assert.isNotOk(element._computeShowOnBehalfOf(message));
+ message.real_author._account_id = 123456;
+ assert.isOk(element._computeShowOnBehalfOf(message));
+ message.updated_by = message.author;
+ delete message.author;
+ assert.isOk(element._computeShowOnBehalfOf(message));
+ delete message.updated_by;
+ assert.isNotOk(element._computeShowOnBehalfOf(message));
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index 7026d3c..cdca365 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -24,6 +24,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-messages-list.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-messages-list></gr-messages-list>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
index 20f9f77..e7d7cf9 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -93,8 +93,7 @@
}
}
</style>
- <div hidden$="[[!loading]]">Loading...</div>
- <div hidden$="[[loading]]">
+ <div>
<hr class="mobile">
<section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden>
<h4>Relation chain</h4>
@@ -158,6 +157,7 @@
</template>
</section>
</div>
+ <div hidden$="[[!loading]]">Loading...</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-related-changes-list.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index 48a2860..5a9642e 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -39,11 +39,26 @@
computed: '_computeConnectedRevisions(change, patchNum, ' +
'_relatedResponse.changes)',
},
- _relatedResponse: Object,
- _submittedTogether: Array,
- _conflicts: Array,
- _cherryPicks: Array,
- _sameTopic: Array,
+ _relatedResponse: {
+ type: Object,
+ value: function() { return {changes: []}; },
+ },
+ _submittedTogether: {
+ type: Array,
+ value: function() { return []; },
+ },
+ _conflicts: {
+ type: Array,
+ value: function() { return []; },
+ },
+ _cherryPicks: {
+ type: Array,
+ value: function() { return []; },
+ },
+ _sameTopic: {
+ type: Array,
+ value: function() { return []; },
+ },
},
behaviors: [
@@ -57,6 +72,7 @@
clear: function() {
this.loading = true;
+ this.hidden = true;
},
reload: function() {
@@ -69,7 +85,7 @@
this._relatedResponse = response;
this.hasParent = this._calculateHasParent(this.change.change_id,
- response.changes);
+ response.changes);
}.bind(this)),
this._getSubmittedTogether().then(function(response) {
@@ -205,11 +221,12 @@
submittedTogether,
conflicts,
cherryPicks,
- sameTopic
+ sameTopic,
];
for (var i = 0; i < results.length; i++) {
if (results[i].length > 0) {
this.hidden = false;
+ this.fire('update', null, {bubbles: false});
return;
}
}
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
index 98cb570d..171f758 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -24,6 +24,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-related-changes-list.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-related-changes-list></gr-related-changes-list>
@@ -67,8 +69,8 @@
},
'9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': {
_number: 4,
- }
- }
+ },
+ },
};
var patchNum = 7;
var relatedChanges = [
@@ -77,8 +79,8 @@
commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
parents: [
{
- commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd'
- }
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ },
],
},
},
@@ -87,8 +89,8 @@
commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
parents: [
{
- commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb'
- }
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ },
],
},
},
@@ -97,8 +99,8 @@
commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
parents: [
{
- commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae'
- }
+ commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ },
],
},
},
@@ -107,8 +109,8 @@
commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
parents: [
{
- commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907'
- }
+ commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
+ },
],
},
},
@@ -117,8 +119,8 @@
commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907',
parents: [
{
- commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce'
- }
+ commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce',
+ },
],
},
},
@@ -127,11 +129,11 @@
commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce',
parents: [
{
- commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75'
- }
+ commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75',
+ },
],
},
- }
+ },
];
var connectedChanges =
@@ -153,8 +155,8 @@
commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8',
parents: [
{
- commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd'
- }
+ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
+ },
],
},
},
@@ -163,8 +165,8 @@
commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd',
parents: [
{
- commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb'
- }
+ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
+ },
],
},
},
@@ -173,8 +175,8 @@
commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb',
parents: [
{
- commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae'
- }
+ commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae',
+ },
],
},
},
@@ -183,8 +185,8 @@
commit: 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b',
parents: [
{
- commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6'
- }
+ commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
+ },
],
},
},
@@ -193,8 +195,8 @@
commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6',
parents: [
{
- commit: 'af815dac54318826b7f1fa468acc76349ffc588e'
- }
+ commit: 'af815dac54318826b7f1fa468acc76349ffc588e',
+ },
],
},
},
@@ -203,11 +205,11 @@
commit: 'af815dac54318826b7f1fa468acc76349ffc588e',
parents: [
{
- commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c'
- }
+ commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c',
+ },
],
},
- }
+ },
];
connectedChanges =
@@ -246,7 +248,7 @@
sandbox.stub(element, '_getCherryPicks',
function() { return Promise.resolve(); });
conflictsStub = sandbox.stub(element, '_getConflicts',
- function() { return Promise.resolve(); });
+ function() { return Promise.resolve(['test data']); });
});
test('request conflicts if open and mergeable', function() {
@@ -282,8 +284,7 @@
assert.isFalse(conflictsStub.called);
});
- test('does not request conflicts if closed and not mergeable',
- function() {
+ test('doesnt request conflicts if closed and not mergeable', function() {
element.patchNum = 7;
element.change = {
change_id: 123,
@@ -311,5 +312,26 @@
true);
});
+
+ test('clear hides', function() {
+ element.loading = false;
+ element.hidden = false;
+ element.clear();
+ assert.isTrue(element.loading);
+ assert.isTrue(element.hidden);
+ });
+
+ test('update fires', function() {
+ var updateHandler = sandbox.stub();
+ element.addEventListener('update', updateHandler);
+
+ element._resultsChanged([], [], [], [], []);
+ assert.isTrue(element.hidden);
+ assert.isFalse(updateHandler.called);
+
+ element._resultsChanged([], [], [], [], ['test']);
+ assert.isFalse(element.hidden);
+ assert.isTrue(updateHandler.called);
+ });
});
</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 9305105..f9ec01c 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
@@ -14,10 +14,11 @@
limitations under the License.
-->
+<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="../../../bower_components/iron-selector/iron-selector.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
<link rel="import" href="../../shared/gr-button/gr-button.html">
<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
@@ -191,6 +192,7 @@
change="[[change]]"
filter="[[filterReviewerSuggestion]]"
pending-confirmation="{{_ccPendingConfirmation}}"
+ allow-any-input
placeholder="Add CC...">
</gr-account-list>
</div>
@@ -263,12 +265,18 @@
</template>
</section>
<section class="draftsContainer" hidden$="[[_computeHideDraftList(diffDrafts)]]">
- <h3>[[_computeDraftsTitle(diffDrafts)]]</h3>
+ <div class="includeComments">
+ <input type="checkbox" id="includeComments"
+ checked="{{_includeComments::change}}">
+ <label for="includeComments">Publish [[_computeDraftsTitle(diffDrafts)]]</label>
+ </div>
<gr-comment-list
+ id="commentList"
comments="[[diffDrafts]]"
change-num="[[change._number]]"
project-config="[[projectConfig]]"
- patch-num="[[patchNum]]"></gr-comment-list>
+ patch-num="[[patchNum]]"
+ hidden$="[[!_includeComments]]"></gr-comment-list>
</section>
<section class="actionsContainer">
<gr-button primary class="action send" on-tap="_sendTapHandler">Send</gr-button>
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 80ccd11..1dd9707 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
@@ -90,6 +90,10 @@
},
_owner: Object,
_pendingConfirmationDetails: Object,
+ _includeComments: {
+ type: Boolean,
+ value: true,
+ },
_reviewers: Array,
_reviewerPendingConfirmation: {
type: Object,
@@ -112,9 +116,14 @@
FocusTarget: FocusTarget,
behaviors: [
+ Gerrit.KeyboardShortcutBehavior,
Gerrit.RESTClientBehavior,
],
+ keyBindings: {
+ 'esc': '_handleEscKey',
+ },
+
observers: [
'_changeUpdated(change.reviewers.*, change.owner, serverConfig)',
'_ccsChanged(_ccs.splices)',
@@ -158,6 +167,10 @@
selectorEl.selectIndex(selectorEl.indexOf(item));
},
+ _handleEscKey: function(e) {
+ this.cancel();
+ },
+
_ccsChanged: function(splices) {
if (splices && splices.indexSplices) {
this._processReviewerChange(splices.indexSplices, ReviewerTypes.CC);
@@ -238,7 +251,7 @@
var reviewerId;
var confirmed;
if (reviewer.account) {
- reviewerId = reviewer.account._account_id;
+ reviewerId = reviewer.account._account_id || reviewer.account.email;
} else if (reviewer.group) {
reviewerId = reviewer.group.id;
confirmed = reviewer.group.confirmed;
@@ -246,9 +259,9 @@
return {reviewer: reviewerId, confirmed: confirmed};
},
- send: function() {
+ send: function(includeComments) {
var obj = {
- drafts: 'PUBLISH_ALL_REVISIONS',
+ drafts: includeComments ? 'PUBLISH_ALL_REVISIONS' : 'KEEP',
labels: {},
};
@@ -284,8 +297,9 @@
}
return this._mapReviewer(reviewer);
}.bind(this));
- if (this.serverConfig.note_db_enabled) {
- this.$$('#ccs').additions().forEach(function(reviewer) {
+ var ccsEl = this.$$('#ccs');
+ if (ccsEl) {
+ ccsEl.additions().forEach(function(reviewer) {
if (reviewer.account) {
accountAdditions[reviewer.account._account_id] = true;
}
@@ -304,6 +318,7 @@
}
this.disabled = false;
this.draft = '';
+ this._includeComments = true;
this.fire('send', null, {bubbles: false});
return accountAdditions;
}.bind(this)).catch(function(err) {
@@ -517,6 +532,10 @@
_cancelTapHandler: function(e) {
e.preventDefault();
+ this.cancel();
+ },
+
+ cancel: function() {
this.fire('cancel', null, {bubbles: false});
this._purgeReviewersPendingRemove(true);
this._rebuildReviewerArrays(this.change.reviewers, this._owner,
@@ -525,8 +544,8 @@
_sendTapHandler: function(e) {
e.preventDefault();
- this.send().then(function(keep) {
- this._purgeReviewersPendingRemove(false, keep);
+ this.send(this._includeComments).then(function(keepReviewers) {
+ this._purgeReviewersPendingRemove(false, keepReviewers);
}.bind(this));
},
@@ -565,6 +584,8 @@
},
_getStorageLocation: function() {
+ // Tests trigger this method without setting change.
+ if (!this.change) { return {}; }
return {
changeNum: this.change._number,
patchNum: this.patchNum,
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 7adc4a3..8aa8dd8 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
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reply-dialog</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-reply-dialog.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-reply-dialog></gr-reply-dialog>
@@ -126,6 +128,69 @@
MockInteractions.tap(element.$$('.cancel'));
});
+ test('default to publishing drafts with reply', function(done) {
+ // Async tick is needed because iron-selector content is distributed and
+ // distributed content requires an observer to be set up.
+ // Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
+ flush(function() {
+ flush(function() {
+ element.draft = 'I wholeheartedly disapprove';
+
+ var saveReviewStub = sandbox.stub(element, '_saveReview',
+ function(review) {
+ assert.deepEqual(review, {
+ drafts: 'PUBLISH_ALL_REVISIONS',
+ labels: {},
+ message: 'I wholeheartedly disapprove',
+ reviewers: [],
+ });
+ assert.isFalse(element.$.commentList.hidden);
+ done();
+ return Promise.resolve({ok: true});
+ });
+
+ // This is needed on non-Blink engines most likely due to the ways in
+ // which the dom-repeat elements are stamped.
+ flush(function() {
+ MockInteractions.tap(element.$$('.send'));
+ });
+ });
+ });
+ });
+
+ test('keep drafts with reply', function(done) {
+ MockInteractions.tap(element.$$('#includeComments'));
+ assert.equal(element._includeComments, false);
+
+ // Async tick is needed because iron-selector content is distributed and
+ // distributed content requires an observer to be set up.
+ // Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
+ flush(function() {
+ flush(function() {
+ element.draft = 'I wholeheartedly disapprove';
+
+ var saveReviewStub = sandbox.stub(element, '_saveReview',
+ function(review) {
+ assert.deepEqual(review, {
+ drafts: 'KEEP',
+ labels: {},
+ message: 'I wholeheartedly disapprove',
+ reviewers: [],
+ });
+ assert.isTrue(element.$.commentList.hidden);
+ done();
+ return Promise.resolve({ok: true});
+ });
+
+ // This is needed on non-Blink engines most likely due to the ways in
+ // which the dom-repeat elements are stamped.
+ flush(function() {
+ MockInteractions.tap(element.$$('.send'));
+ });
+ });
+ });
+ });
+
test('label picker', function(done) {
element.revisions = {};
element.patchNum = '';
@@ -147,7 +212,7 @@
'iron-selector[data-label="Verified"] > ' +
'gr-button[data-value="-1"]'));
- var saveReviewStub = sinon.stub(element, '_saveReview',
+ var saveReviewStub = sandbox.stub(element, '_saveReview',
function(review) {
assert.deepEqual(review, {
drafts: 'PUBLISH_ALL_REVISIONS',
@@ -165,7 +230,6 @@
assert.isFalse(element.disabled,
'Element should be enabled when done sending reply.');
assert.equal(element.draft.length, 0);
- saveReviewStub.restore();
done();
});
@@ -440,14 +504,13 @@
test('only send labels that have changed', function(done) {
flush(function() {
- var saveReviewStub = sinon.stub(element, '_saveReview',
+ var saveReviewStub = sandbox.stub(element, '_saveReview',
function(review) {
assert.deepEqual(review.labels, {Verified: -1});
return Promise.resolve({ok: true});
});
element.addEventListener('send', function() {
- saveReviewStub.restore();
done();
});
// Without wrapping this test in flush(), the below two calls to
@@ -621,5 +684,14 @@
done();
});
});
+
+ test('emits cancel on esc key', function() {
+ var cancelHandler = sandbox.spy();
+ element.addEventListener('cancel', cancelHandler);
+ MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc');
+ flushAsynchronousOperations();
+
+ assert.isTrue(cancelHandler.called);
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 435b7de..9a84c95 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -59,7 +59,6 @@
<template is="dom-repeat" items="[[_reviewers]]" as="reviewer">
<gr-account-chip class="reviewer" account="[[reviewer]]"
on-remove="_handleRemove"
- data-account-id$="[[reviewer._account_id]]"
removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]">
</gr-account-chip>
</template>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index 72a7c9b..52b90ee 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -87,9 +87,11 @@
_computeCanRemoveReviewer: function(reviewer, mutable) {
if (!mutable) { return false; }
+ var current;
for (var i = 0; i < this.change.removable_reviewers.length; i++) {
- if (this.change.removable_reviewers[i]._account_id ==
- reviewer._account_id) {
+ current = this.change.removable_reviewers[i];
+ if (current._account_id === reviewer._account_id ||
+ (!reviewer._account_id && current.email === reviewer.email)) {
return true;
}
}
@@ -99,7 +101,8 @@
_handleRemove: function(e) {
e.preventDefault();
var target = Polymer.dom(e).rootTarget;
- var accountID = parseInt(target.getAttribute('data-account-id'), 10);
+ if (!target.account) { return; }
+ var accountID = target.account._account_id || target.account.email;
this.disabled = true;
this._xhrPromise =
this._removeReviewer(accountID).then(function(response) {
@@ -110,7 +113,8 @@
['REVIEWER', 'CC'].forEach(function(type) {
reviewers[type] = reviewers[type] || [];
for (var i = 0; i < reviewers[type].length; i++) {
- if (reviewers[type][i]._account_id == accountID) {
+ if (reviewers[type][i]._account_id == accountID ||
+ reviewers[type][i].email == accountID) {
this.splice('change.reviewers.' + type, i, 1);
break;
}
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index d2ee15e..7c8a76f 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reviewer-list</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-reviewer-list.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-reviewer-list></gr-reviewer-list>
@@ -88,7 +90,10 @@
name: 'Diane Nguyen',
email: 'macarthurfellow2B@juno.com',
},
- ]
+ {
+ email: 'test@e.mail',
+ },
+ ],
},
removable_reviewers: [
{
@@ -100,14 +105,17 @@
name: 'Diane Nguyen',
email: 'macarthurfellow2B@juno.com',
},
- ]
+ {
+ email: 'test@e.mail',
+ },
+ ],
};
flushAsynchronousOperations();
var chips =
Polymer.dom(element.root).querySelectorAll('gr-account-chip');
- assert.equal(chips.length, 3);
+ assert.equal(chips.length, 4);
Array.from(chips).forEach(function(el) {
- var accountID = parseInt(el.getAttribute('data-account-id'), 10);
+ var accountID = el.account._account_id || el.account.email;
assert.ok(accountID);
var buttonEl = el.$$('gr-button');
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 4011135..65c99c0 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -14,37 +14,76 @@
(function() {
'use strict';
+ var INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g;
+
Polymer({
is: 'gr-account-dropdown',
properties: {
account: Object,
- _hasAvatars: Boolean,
links: {
type: Array,
- value: [
- {name: 'Settings', url: '/settings'},
- {name: 'Switch account', url: '/switch-account'},
- {name: 'Sign out', url: '/logout'},
- ],
+ computed: '_getLinks(_switchAccountUrl, _path)',
},
topContent: {
type: Array,
computed: '_getTopContent(account)',
},
+ _path: {
+ type: String,
+ value: '/',
+ },
+ _hasAvatars: Boolean,
+ _switchAccountUrl: String,
},
attached: function() {
+ this._handleLocationChange();
+ this.listen(window, 'location-change', '_handleLocationChange');
this.$.restAPI.getConfig().then(function(cfg) {
+ if (cfg && cfg.auth && cfg.auth.switch_account_url) {
+ this._switchAccountUrl = cfg.auth.switch_account_url;
+ } else {
+ this._switchAccountUrl = null;
+ }
this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
}.bind(this));
},
+ detached: function() {
+ this.unlisten(window, 'location-change', '_handleLocationChange');
+ },
+
+ _getLinks: function(switchAccountUrl, path) {
+ var links = [{name: 'Settings', url: '/settings'}];
+ if (switchAccountUrl) {
+ var replacements = {path: path};
+ var url = this._interpolateUrl(switchAccountUrl, replacements);
+ links.push({name: 'Switch account', url: url});
+ }
+ links.push({name: 'Sign out', url: '/logout'});
+ return links;
+ },
+
_getTopContent: function(account) {
+ // if (!account) { return []; }
return [
{text: account.name, bold: true},
{text: account.email},
];
},
+
+ _handleLocationChange: function() {
+ this._path =
+ window.location.pathname +
+ window.location.search +
+ window.location.hash;
+ },
+
+ _interpolateUrl: function(url, replacements) {
+ return url.replace(INTERPOLATE_URL_PATTERN, function(match, p1) {
+ return replacements[p1] || '';
+ });
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
index ec9141f..3e109a8 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-dropdown</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-account-dropdown.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-account-dropdown></gr-account-dropdown>
@@ -46,5 +48,38 @@
assert.deepEqual(element.topContent,
[{text: 'John Doe', bold: true}, {text: 'john@doe.com'}]);
});
+
+ test('switch account', function() {
+ // No switch account link.
+ assert.equal(element._getLinks(null).length, 2);
+
+ // Unparameterized switch account link.
+ var links = element._getLinks('/switch-account');
+ assert.equal(links.length, 3);
+ assert.deepEqual(links[1],
+ {name: 'Switch account', url: '/switch-account'});
+
+ // Parameterized switch account link.
+ links = element._getLinks('/switch-account${path}', '/c/123');
+ assert.equal(links.length, 3);
+ assert.deepEqual(links[1],
+ {name: 'Switch account', url: '/switch-account/c/123'});
+ });
+
+ test('_interpolateUrl', function() {
+ var replacements = {
+ 'foo': 'bar',
+ 'test': 'TEST',
+ };
+ var interpolate = function(url) {
+ return element._interpolateUrl(url, replacements);
+ };
+
+ assert.equal(interpolate('test'), 'test');
+ assert.equal(interpolate('${test}'), 'TEST');
+ assert.equal(
+ interpolate('${}, ${test}, ${TEST}, ${foo}'),
+ '${}, TEST, , bar');
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
index 80f293d..2d7d2a9 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
@@ -24,4 +24,3 @@
</template>
<script src="gr-error-manager.js"></script>
</dom-module>
-
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 8209cde..870e7ea 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
@@ -15,8 +15,8 @@
'use strict';
var HIDE_ALERT_TIMEOUT_MS = 5000;
- var CHECK_SIGN_IN_INTERVAL_MS = 60*1000;
- var STALE_CREDENTIAL_THRESHOLD_MS = 10*60*1000;
+ var CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
+ var STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000;
var SIGN_IN_WIDTH_PX = 690;
var SIGN_IN_HEIGHT_PX = 500;
var TOO_MANY_FILES = 'too many files to find conflicts';
@@ -52,12 +52,14 @@
this.listen(document, 'network-error', '_handleNetworkError');
this.listen(document, 'show-alert', '_handleShowAlert');
this.listen(document, 'visibilitychange', '_handleVisibilityChange');
+ this.listen(document, 'show-auth-required', '_handleAuthRequired');
},
detached: function() {
this._clearHideAlertHandle();
this.unlisten(document, 'server-error', '_handleServerError');
this.unlisten(document, 'network-error', '_handleNetworkError');
+ this.unlisten(document, 'show-auth-required', '_handleAuthRequired');
this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
},
@@ -65,13 +67,18 @@
return msg.indexOf(TOO_MANY_FILES) > -1;
},
+ _handleAuthRequired: function() {
+ this._showAuthErrorAlert(
+ 'Log in is required to perform that action.', 'Log in.');
+ },
+
_handleServerError: function(e) {
if (e.detail.response.status === 403) {
this._getLoggedIn().then(function(loggedIn) {
if (loggedIn) {
// The app was logged at one point and is now getting auth errors.
// This indicates the auth token is no longer valid.
- this._showAuthErrorAlert();
+ this._showAuthErrorAlert('Auth error', 'Refresh credentials.');
}
}.bind(this));
} else {
@@ -121,12 +128,12 @@
}
},
- _showAuthErrorAlert: function() {
+ _showAuthErrorAlert: function(errorText, actionText) {
// TODO(viktard): close alert if it's not for auth error.
if (this._alertElement) { return; }
this._alertElement = this._createToastAlert();
- this._alertElement.show('Auth error', 'Refresh credentials.');
+ this._alertElement.show(errorText, actionText);
this.listen(this._alertElement, 'action', '_createLoginPopup');
this._refreshingCredentials = true;
@@ -147,17 +154,12 @@
// undefined).
if (document.hidden !== false) { return; }
- // If we're currently in a credential refresh, flush the debouncer so that
- // it can be checked immediately.
- if (this._refreshingCredentials) {
- this.flushDebouncer('checkLoggedIn');
- return;
- }
-
- // If the credentials are old, request them to confirm their validity or
- // (display an auth toast if it fails).
+ // If not currently refreshing credentials and the credentials are old,
+ // request them to confirm their validity or (display an auth toast if it
+ // fails).
var timeSinceLastCheck = Date.now() - this._lastCredentialCheck;
- if (this.knownAccountId !== undefined &&
+ if (!this._refreshingCredentials &&
+ this.knownAccountId !== undefined &&
timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) {
this._lastCredentialCheck = Date.now();
this.$.restAPI.checkCredentials();
@@ -205,13 +207,19 @@
'top=' + top,
];
window.open('/login/%3FcloseAfterLogin', '_blank', options.join(','));
+ this.listen(window, 'focus', '_handleWindowFocus');
},
_handleCredentialRefreshed: function() {
+ this.unlisten(window, 'focus', '_handleWindowFocus');
this._refreshingCredentials = false;
this.unlisten(this._alertElement, 'action', '_createLoginPopup');
this._hideAlert();
this._showAlert('Credentials refreshed.');
},
+
+ _handleWindowFocus: function() {
+ this.flushDebouncer('checkLoggedIn');
+ },
});
})();
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 781220a..8bab82e 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
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-error-manager</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-error-manager.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-error-manager></gr-error-manager>
@@ -56,6 +58,13 @@
});
});
+ test('show logged in error', function() {
+ sandbox.stub(element, '_showAuthErrorAlert');
+ element.fire('show-auth-required');
+ assert.isTrue(element._showAuthErrorAlert.calledWithExactly(
+ 'Log in is required to perform that action.', 'Log in.'));
+ });
+
test('show normal server error', function(done) {
var showAlertStub = sandbox.stub(element, '_showAlert');
var textSpy = sandbox.spy(function() { return Promise.resolve('ZOMG'); });
@@ -123,6 +132,7 @@
var hideToastSpy = sandbox.spy(toast, 'hide');
+ element._handleWindowFocus();
assert.isTrue(refreshStub.called);
element.flushDebouncer('checkLoggedIn');
flush(function() {
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
index 75723b9..eebf397 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-main-header</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-main-header.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-main-header></gr-main-header>
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 fd8a73b..2720ebd 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
@@ -18,11 +18,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reporting</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="gr-reporting.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-reporting></gr-reporting>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
index 0512662..3ddc96b 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -18,7 +18,7 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-search-bar</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/page/page.js"></script>
@@ -26,6 +26,8 @@
<link rel="import" href="gr-search-bar.html">
<script src="../../../scripts/util.js"></script>
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-search-bar></gr-search-bar>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
index 6b1e59e..838938b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
@@ -41,44 +41,89 @@
var tr = this._createElement('tr');
tr.appendChild(this._createElement('td'));
- tr.appendChild(this._createImageCell(this._baseImage, 'left'));
+ tr.appendChild(this._createImageCell(this._baseImage, 'left', section));
tr.appendChild(this._createElement('td'));
- tr.appendChild(this._createImageCell(this._revisionImage, 'right'));
+ tr.appendChild(this._createImageCell(
+ this._revisionImage, 'right', section));
section.appendChild(tr);
};
- GrDiffBuilderImage.prototype._createImageCell = function(image, className) {
+ GrDiffBuilderImage.prototype._createImageCell =
+ function(image, className, section) {
var td = this._createElement('td', className);
if (image) {
var imageEl = this._createElement('img');
+ imageEl.onload = function() {
+ image._height = imageEl.naturalHeight;
+ image._width = imageEl.naturalWidth;
+ this._updateImageLabel(section, className, image);
+ }.bind(this);
imageEl.src = 'data:' + image.type + ';base64, ' + image.body;
- image._height = imageEl.naturalHeight;
- image._width = imageEl.naturalWidth;
imageEl.addEventListener('error', function(e) {
imageEl.remove();
td.textContent = '[Image failed to load]';
});
td.appendChild(imageEl);
- }
return td;
+ }
+ };
+
+ GrDiffBuilderImage.prototype._updateImageLabel =
+ function(section, className, image) {
+ var label = Polymer.dom(section)
+ .querySelector('.' + className + ' span.label');
+ this._setLabelText(label, image);
+ };
+
+ GrDiffBuilderImage.prototype._setLabelText = function(label, image) {
+ label.textContent = this._getImageLabel(image);
};
GrDiffBuilderImage.prototype._emitImageLabels = function(section) {
var tr = this._createElement('tr');
+ var addNamesInLabel = false;
+
+ if (this._baseImage._name !== this._revisionImage._name) {
+ addNamesInLabel = true;
+ }
+
tr.appendChild(this._createElement('td'));
var td = this._createElement('td', 'left');
var label = this._createElement('label');
- label.textContent = this._getImageLabel(this._baseImage);
+ var nameSpan;
+ var labelSpan = this._createElement('span', 'label');
+
+ if (addNamesInLabel) {
+ nameSpan = this._createElement('span', 'name');
+ nameSpan.textContent = this._baseImage._name;
+ label.appendChild(nameSpan);
+ label.appendChild(this._createElement('br'));
+ }
+
+ this._setLabelText(labelSpan, this._baseImage, addNamesInLabel);
+
+ label.appendChild(labelSpan);
td.appendChild(label);
tr.appendChild(td);
tr.appendChild(this._createElement('td'));
td = this._createElement('td', 'right');
label = this._createElement('label');
- label.textContent = this._getImageLabel(this._revisionImage);
+ labelSpan = this._createElement('span', 'label');
+
+ if (addNamesInLabel) {
+ nameSpan = this._createElement('span', 'name');
+ nameSpan.textContent = this._revisionImage._name;
+ label.appendChild(nameSpan);
+ label.appendChild(this._createElement('br'));
+ }
+
+ this._setLabelText(labelSpan, this._revisionImage, addNamesInLabel);
+
+ label.appendChild(labelSpan);
td.appendChild(label);
tr.appendChild(td);
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 a936934..de93a69 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
@@ -145,7 +145,8 @@
reporting.time(TimingLabel.TOTAL);
reporting.time(TimingLabel.CONTENT);
this.dispatchEvent(new CustomEvent('render-start', {bubbles: true}));
- return this.$.processor.process(this.diff.content).then(function() {
+ return this.$.processor.process(this.diff.content, this.isImageDiff)
+ .then(function() {
if (this.isImageDiff) {
this._builder.renderDiffImages();
}
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 d53929e..591fa9c 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
@@ -18,7 +18,7 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-builder</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/util.js"></script>
<script src="../gr-diff/gr-diff-line.js"></script>
@@ -29,6 +29,8 @@
<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
<link rel="import" href="gr-diff-builder.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-diff-builder>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
index bc08bab..53a8e81 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
@@ -25,6 +25,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-comment-thread-group.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-diff-comment-thread-group></gr-diff-comment-thread-group>
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 37a0e2b..be88e476 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
@@ -167,6 +167,7 @@
var c1Date = c1.__date || util.parseDate(c1.updated);
var c2Date = c2.__date || util.parseDate(c2.updated);
var dateCompare = c1Date - c2Date;
+ if (!c1.id || !c1.id.localeCompare) { return 0; }
// If same date, fall back to sorting by id.
return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
});
@@ -328,7 +329,7 @@
var index = this._indexOf(comment, this.comments);
if (index === -1) {
// This should never happen: comment belongs to another thread.
- console.error('Comment update for another comment thread.');
+ console.warn('Comment update for another comment thread.');
return;
}
this.set(['comments', index], comment);
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 cd1fdfb..546308e 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
@@ -25,6 +25,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-comment-thread.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-diff-comment-thread></gr-diff-comment-thread>
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 2881fc1..919a64f 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
@@ -26,6 +26,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-comment.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-diff-comment></gr-diff-comment>
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 f1f3810..a77c617 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
@@ -18,7 +18,7 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-cursor</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/util.js"></script>
@@ -27,6 +27,8 @@
<link rel="import" href="./gr-diff-cursor.html">
<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<mock-diff-response></mock-diff-response>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
index 27a684d..0a03539 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-annotation</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="gr-annotation.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<div>Lorem ipsum dolor sit amet, suspendisse inceptos vehicula</div>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index b1b6057..827437c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-highlight</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-highlight.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<style>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
index 999f005..06f617a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-preferences</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-preferences.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-diff-preferences></gr-diff-preferences>
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 95ff5b7..62e8915 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
@@ -103,11 +103,14 @@
* @return {Promise} A promise that resolves when the diff is completely
* processed.
*/
- process: function(content) {
- return new Promise(function(resolve) {
- this.groups = [];
- this.push('groups', this._makeFileComments());
+ process: function(content, isImageDiff) {
+ this.groups = [];
+ this.push('groups', this._makeFileComments());
+ // If image diff, only render the file lines.
+ if (isImageDiff) { return Promise.resolve(); }
+
+ return new Promise(function(resolve) {
var state = {
lineNums: {left: 0, right: 0},
sectionIndex: 0,
@@ -117,7 +120,6 @@
var currentBatch = 0;
var nextStep = function() {
-
if (this._isScrolling) {
this.async(nextStep, 100);
return;
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 7886ef2..687b3dd 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
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-processor test</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-processor.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-diff-processor></gr-diff-processor>
@@ -457,6 +459,23 @@
assert.equal(element.groups.length, 33);
});
+ test('image diffs', function() {
+ var contentRow = {
+ ab: [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ ]
+ };
+ var content = _.times(200, _.constant(contentRow));
+ sandbox.stub(element, 'async');
+ element.process(content, true);
+ assert.equal(element.groups.length, 1);
+
+ // Image diffs don't process content, just the 'FILE' line.
+ assert.equal(element.groups[0].lines.length, 1);
+ });
+
+
suite('gr-diff-processor helpers', function() {
var rows;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
index 86edf6b..3eeba90 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-selection</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-selection.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-diff-selection>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 1558d0b..9ec52d2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -229,6 +229,9 @@
href$="[[_computeNavLinkURL(_path, _fileList, -1, 1)]]">Prev</a>
/
<a class="navLink"
+ href$="[[_computeUpURL(_changeNum, _patchRange, _change, _change.revisions)]]">Up</a>
+ /
+ <a class="navLink"
href$="[[_computeNavLinkURL(_path, _fileList, 1, 1)]]">Next</a>
</div>
</header>
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 8d54b2a..b795d64 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
@@ -374,6 +374,13 @@
this._change && this._change.revisions));
},
+ _computeUpURL: function(changeNum, patchRange, change, changeRevisions) {
+ return this._getChangePath(
+ changeNum,
+ patchRange,
+ change && changeRevisions);
+ },
+
_navToFile: function(path, fileList, direction) {
var url = this._computeNavLinkURL(path, fileList, direction);
if (!url) { return; }
@@ -573,7 +580,11 @@
},
_handleFileTap: function(e) {
- this.$.dropdown.close();
+ // async is needed so that that the click event is fired before the
+ // dropdown closes (This was a bug for touch devices).
+ this.async(function() {
+ this.$.dropdown.close();
+ }, 1);
},
_handleMobileSelectChange: function(e) {
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 99dcd41..4759086 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
@@ -18,7 +18,7 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff-view</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/page/page.js"></script>
<script src="../../../scripts/util.js"></script>
@@ -26,6 +26,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-diff-view.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-diff-view></gr-diff-view>
@@ -366,31 +368,40 @@
assert.equal(linkEls[2].getAttribute('href'), '/c/42/5..10/wheatley.md');
});
- test('prev/next links', function() {
+ test('prev/up/next links', function() {
element._changeNum = '42';
element._patchRange = {
basePatchNum: 'PARENT',
patchNum: '10',
};
+ element._change = {
+ revisions: {
+ a: {_number: 10},
+ },
+ };
element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
element._path = 'glados.txt';
flushAsynchronousOperations();
var linkEls = Polymer.dom(element.root).querySelectorAll('.navLink');
- assert.equal(linkEls.length, 2);
+ assert.equal(linkEls.length, 3);
assert.equal(linkEls[0].getAttribute('href'), '/c/42/10/chell.go');
- assert.equal(linkEls[1].getAttribute('href'), '/c/42/10/wheatley.md');
+ assert.equal(linkEls[1].getAttribute('href'), '/c/42/');
+ assert.equal(linkEls[2].getAttribute('href'), '/c/42/10/wheatley.md');
element._path = 'wheatley.md';
flushAsynchronousOperations();
assert.equal(linkEls[0].getAttribute('href'), '/c/42/10/glados.txt');
- assert.isFalse(linkEls[1].hasAttribute('href'));
+ assert.equal(linkEls[1].getAttribute('href'), '/c/42/');
+ assert.isFalse(linkEls[2].hasAttribute('href'));
element._path = 'chell.go';
flushAsynchronousOperations();
assert.isFalse(linkEls[0].hasAttribute('href'));
- assert.equal(linkEls[1].getAttribute('href'), '/c/42/10/glados.txt');
+ assert.equal(linkEls[1].getAttribute('href'), '/c/42/');
+ assert.equal(linkEls[2].getAttribute('href'), '/c/42/10/glados.txt');
element._path = 'not_a_real_file';
flushAsynchronousOperations();
assert.equal(linkEls[0].getAttribute('href'), '/c/42/10/wheatley.md');
- assert.equal(linkEls[1].getAttribute('href'), '/c/42/10/chell.go');
+ assert.equal(linkEls[1].getAttribute('href'), '/c/42/');
+ assert.equal(linkEls[2].getAttribute('href'), '/c/42/10/chell.go');
});
test('download link', function() {
@@ -406,27 +417,36 @@
'/changes/42/revisions/10/patch?zip&path=glados.txt');
});
- test('prev/next links with patch range', function() {
+ test('prev/up/next links with patch range', function() {
element._changeNum = '42';
element._patchRange = {
basePatchNum: '5',
patchNum: '10',
};
+ element._change = {
+ revisions: {
+ a: {_number: 5},
+ b: {_number: 10},
+ },
+ };
element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
element._path = 'glados.txt';
flushAsynchronousOperations();
var linkEls = Polymer.dom(element.root).querySelectorAll('.navLink');
- assert.equal(linkEls.length, 2);
+ assert.equal(linkEls.length, 3);
assert.equal(linkEls[0].getAttribute('href'), '/c/42/5..10/chell.go');
- assert.equal(linkEls[1].getAttribute('href'), '/c/42/5..10/wheatley.md');
+ assert.equal(linkEls[1].getAttribute('href'), '/c/42/5..10');
+ assert.equal(linkEls[2].getAttribute('href'), '/c/42/5..10/wheatley.md');
element._path = 'wheatley.md';
flushAsynchronousOperations();
assert.equal(linkEls[0].getAttribute('href'), '/c/42/5..10/glados.txt');
- assert.isFalse(linkEls[1].hasAttribute('href'));
+ assert.equal(linkEls[1].getAttribute('href'), '/c/42/5..10');
+ assert.isFalse(linkEls[2].hasAttribute('href'));
element._path = 'chell.go';
flushAsynchronousOperations();
assert.isFalse(linkEls[0].hasAttribute('href'));
- assert.equal(linkEls[1].getAttribute('href'), '/c/42/5..10/glados.txt');
+ assert.equal(linkEls[1].getAttribute('href'), '/c/42/5..10');
+ assert.equal(linkEls[2].getAttribute('href'), '/c/42/5..10/glados.txt');
});
test('file review status', function(done) {
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 05a7f72..52e869f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -32,6 +32,12 @@
* @event line-selected
*/
+ /**
+ * Fired if being logged in is required.
+ *
+ * @event show-auth-required
+ */
+
properties: {
changeNum: String,
noAutoRender: {
@@ -146,7 +152,10 @@
addDraftAtLine: function(el) {
this._selectLine(el);
this._getLoggedIn().then(function(loggedIn) {
- if (!loggedIn) { return; }
+ if (!loggedIn) {
+ this.fire('show-auth-required');
+ return;
+ }
var value = el.getAttribute('data-value');
if (value === GrDiffLine.FILE) {
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 9288e92..ad7a260 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
@@ -18,7 +18,7 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-diff</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/util.js"></script>
@@ -26,6 +26,8 @@
<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
<link rel="import" href="gr-diff.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-diff></gr-diff>
@@ -60,6 +62,17 @@
assert.isFalse(element.classList.contains('no-left'));
});
+ test('addDraftAtLine', function(done) {
+ sandbox.stub(element, '_selectLine');
+ var loggedInErrorSpy = sandbox.spy();
+ element.addEventListener('show-auth-required', loggedInErrorSpy);
+ element.addDraftAtLine();
+ flush(function() {
+ assert.isTrue(loggedInErrorSpy.called);
+ done();
+ });
+ });
+
test('view does not start with displayLine classList', function() {
assert.isFalse(
element.$$('.diffContainer').classList.contains('displayLine'));
@@ -273,101 +286,219 @@
assert.equal(threadLength, 2);
});
- test('renders image diffs', function(done) {
- var mockDiff = {
- meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
- meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560},
- intraline_status: 'OK',
- change_type: 'MODIFIED',
- diff_header: [
- 'diff --git a/carrot.jpg b/carrot.jpg',
- 'index 2adc47d..f9c2f2c 100644',
- '--- a/carrot.jpg',
- '+++ b/carrot.jpg',
- 'Binary files differ',
- ],
- content: [{skip: 66}],
- binary: true,
- };
- var mockFile1 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAEwsA' +
- 'AAAAAAAAAAAAAAAA/w==',
- type: 'image/bmp',
- };
- var mockFile2 = {
- body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAEwsA' +
- 'AAAAAAAAAAAA/////w==',
- type: 'image/bmp'
- };
- var mockCommit = {
- commit: '9a1a1d10baece5efbba10bc4ccf808a67a50ac0a',
- parents: [{
- commit: '7338aa9adfe57909f1fdaf88975cdea467d3382f',
- subject: 'Added a carrot',
- }],
- author: {
- name: 'Wyatt Allen',
- email: 'wyatta@google.com',
- date: '2016-05-23 21:44:51.000000000',
- tz: -420,
- },
- committer: {
- name: 'Wyatt Allen',
- email: 'wyatta@google.com',
- date: '2016-05-25 00:25:41.000000000',
- tz: -420,
- },
- subject: 'Updated the carrot',
- message: 'Updated the carrot\n\nChange-Id: Iabcd123\n',
- };
- var mockComments = {baseComments: [], comments: []};
-
+ suite('image diffs', function() {
+ var mockFile1;
+ var mockFile2;
var stubs = [];
- stubs.push(sandbox.stub(element, '_getDiff',
- function() { return Promise.resolve(mockDiff); }));
- stubs.push(sandbox.stub(element.$.restAPI, 'getCommitInfo',
- function() { return Promise.resolve(mockCommit); }));
- stubs.push(sandbox.stub(element.$.restAPI,
- 'getCommitFileContents',
- function() { return Promise.resolve(mockFile1); }));
- stubs.push(sandbox.stub(element.$.restAPI,
- 'getChangeFileContents',
- function() { return Promise.resolve(mockFile2); }));
- stubs.push(sandbox.stub(element.$.restAPI, '_getDiffComments',
- function() { return Promise.resolve(mockComments); }));
- stubs.push(sandbox.stub(element.$.restAPI, 'getDiffDrafts',
- function() { return Promise.resolve(mockComments); }));
+ setup(function() {
+ mockFile1 = {
+ body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAAAAAA/w==',
+ type: 'image/bmp',
+ };
+ mockFile2 = {
+ body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' +
+ 'wsAAAAAAAAAAAAA/////w==',
+ type: 'image/bmp'
+ };
+ var mockCommit = {
+ commit: '9a1a1d10baece5efbba10bc4ccf808a67a50ac0a',
+ parents: [{
+ commit: '7338aa9adfe57909f1fdaf88975cdea467d3382f',
+ subject: 'Added a carrot',
+ }],
+ author: {
+ name: 'Wyatt Allen',
+ email: 'wyatta@google.com',
+ date: '2016-05-23 21:44:51.000000000',
+ tz: -420,
+ },
+ committer: {
+ name: 'Wyatt Allen',
+ email: 'wyatta@google.com',
+ date: '2016-05-25 00:25:41.000000000',
+ tz: -420,
+ },
+ subject: 'Updated the carrot',
+ message: 'Updated the carrot\n\nChange-Id: Iabcd123\n',
+ };
+ var mockComments = {baseComments: [], comments: []};
- element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
+ stubs.push(sandbox.stub(element.$.restAPI, 'getCommitInfo',
+ function() { return Promise.resolve(mockCommit); }));
+ stubs.push(sandbox.stub(element.$.restAPI,
+ 'getCommitFileContents',
+ function() { return Promise.resolve(mockFile1); }));
+ stubs.push(sandbox.stub(element.$.restAPI,
+ 'getChangeFileContents',
+ function() { return Promise.resolve(mockFile2); }));
+ stubs.push(sandbox.stub(element.$.restAPI, '_getDiffComments',
+ function() { return Promise.resolve(mockComments); }));
+ stubs.push(sandbox.stub(element.$.restAPI, 'getDiffDrafts',
+ function() { return Promise.resolve(mockComments); }));
- var rendered = function() {
- // Recognizes that it should be an image diff.
- assert.isTrue(element.isImageDiff);
- assert.instanceOf(element.$.diffBuilder._builder, GrDiffBuilderImage);
+ element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
- // Left image rendered with the parent commit's version of the file.
- var leftInmage = element.$.diffTable.querySelector('td.left img');
- assert.isOk(leftInmage);
- assert.equal(leftInmage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile1.body);
+ });
- // Right image rendered with this change's revision of the image.
- var rightInmage = element.$.diffTable.querySelector('td.right img');
- assert.isOk(rightInmage);
- assert.equal(rightInmage.getAttribute('src'),
- 'data:image/bmp;base64, ' + mockFile2.body);
+ test('renders image diffs with same file name', function(done) {
+ var mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot.jpg',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.jpg',
+ '+++ b/carrot.jpg',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ stubs.push(sandbox.stub(element, '_getDiff',
+ function() { return Promise.resolve(mockDiff); }));
- // Cleanup.
- element.removeEventListener('render', rendered);
+ var rendered = function() {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diffBuilder._builder, GrDiffBuilderImage);
- done();
- };
+ // Left image rendered with the parent commit's version of the file.
+ var leftImage = element.$.diffTable.querySelector('td.left img');
+ var leftLabel = element.$.diffTable.querySelector('td.left label');
+ var leftLabelContent = leftLabel.querySelector('.label');
+ var leftLabelName = leftLabel.querySelector('.name');
- element.addEventListener('render', rendered);
+ var rightImage = element.$.diffTable.querySelector('td.right img');
+ var rightLabel = element.$.diffTable.querySelector(
+ 'td.right label');
+ var rightLabelContent = rightLabel.querySelector('.label');
+ var rightLabelName = rightLabel.querySelector('.name');
- element.$.restAPI.getDiffPreferences().then(function(prefs) {
- element.prefs = prefs;
- element.reload();
+ assert.isNotOk(rightLabelName);
+ assert.isNotOk(leftLabelName);
+
+ var leftLoaded = false;
+ var rightLoaded = false;
+
+ leftImage.addEventListener('load', function() {
+ assert.isOk(leftImage);
+ assert.equal(leftImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile1.body);
+ assert.equal(leftLabelContent.textContent, '1⨉1 image/bmp');
+ leftLoaded = true;
+ if (rightLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+
+ rightImage.addEventListener('load', function() {
+ assert.isOk(rightImage);
+ assert.equal(rightImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile2.body);
+ assert.equal(rightLabelContent.textContent, '1⨉1 image/bmp');
+
+ rightLoaded = true;
+ if (leftLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+
+ };
+
+ element.addEventListener('render', rendered);
+
+ element.$.restAPI.getDiffPreferences().then(function(prefs) {
+ element.prefs = prefs;
+ element.reload();
+ });
+ });
+
+ test('renders image diffs with a different file name', function(done) {
+ var mockDiff = {
+ meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+ meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg',
+ lines: 560},
+ intraline_status: 'OK',
+ change_type: 'MODIFIED',
+ diff_header: [
+ 'diff --git a/carrot.jpg b/carrot2.jpg',
+ 'index 2adc47d..f9c2f2c 100644',
+ '--- a/carrot.jpg',
+ '+++ b/carrot2.jpg',
+ 'Binary files differ',
+ ],
+ content: [{skip: 66}],
+ binary: true,
+ };
+ stubs.push(sandbox.stub(element, '_getDiff',
+ function() { return Promise.resolve(mockDiff); }));
+
+ var rendered = function() {
+ // Recognizes that it should be an image diff.
+ assert.isTrue(element.isImageDiff);
+ assert.instanceOf(
+ element.$.diffBuilder._builder, GrDiffBuilderImage);
+
+ // Left image rendered with the parent commit's version of the file.
+ var leftImage = element.$.diffTable.querySelector('td.left img');
+ var leftLabel = element.$.diffTable.querySelector('td.left label');
+ var leftLabelContent = leftLabel.querySelector('.label');
+ var leftLabelName = leftLabel.querySelector('.name');
+
+ var rightImage = element.$.diffTable.querySelector('td.right img');
+ var rightLabel = element.$.diffTable.querySelector(
+ 'td.right label');
+ var rightLabelContent = rightLabel.querySelector('.label');
+ var rightLabelName = rightLabel.querySelector('.name');
+
+ assert.isOk(rightLabelName);
+ assert.isOk(leftLabelName);
+ assert.equal(leftLabelName.textContent, mockDiff.meta_a.name);
+ assert.equal(rightLabelName.textContent, mockDiff.meta_b.name);
+
+ var leftLoaded = false;
+ var rightLoaded = false;
+
+ leftImage.addEventListener('load', function() {
+ assert.isOk(leftImage);
+ assert.equal(leftImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile1.body);
+ assert.equal(leftLabelContent.textContent, '1⨉1 image/bmp');
+ leftLoaded = true;
+ if (rightLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+
+ rightImage.addEventListener('load', function() {
+ assert.isOk(rightImage);
+ assert.equal(rightImage.getAttribute('src'),
+ 'data:image/bmp;base64, ' + mockFile2.body);
+ assert.equal(rightLabelContent.textContent, '1⨉1 image/bmp');
+
+ rightLoaded = true;
+ if (leftLoaded) {
+ element.removeEventListener('render', rendered);
+ done();
+ }
+ });
+
+ };
+
+ element.addEventListener('render', rendered);
+
+ element.$.restAPI.getDiffPreferences().then(function(prefs) {
+ element.prefs = prefs;
+ element.reload();
+ });
});
});
@@ -578,6 +709,20 @@
});
});
+ test('addDraftAtLine', function(done) {
+ var fakeLineEl = {getAttribute: sandbox.stub().returns(42)};
+ sandbox.stub(element, '_selectLine');
+ sandbox.stub(element, '_addDraft');
+ var loggedInErrorSpy = sandbox.spy();
+ element.addEventListener('show-auth-required', loggedInErrorSpy);
+ element.addDraftAtLine(fakeLineEl);
+ flush(function() {
+ assert.isFalse(loggedInErrorSpy.called);
+ assert.isTrue(element._addDraft.calledWithExactly(fakeLineEl, 42));
+ done();
+ });
+ });
+
suite('handle comment-update', function() {
setup(function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index 68eeaa9..00d73bf 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -18,13 +18,15 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-patch-range-select</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../bower_components/page/page.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-patch-range-select.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-patch-range-select auto></gr-patch-range-select>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
index 85c7886..20fba4d 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -18,13 +18,15 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-ranged-comment-layer</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../gr-diff/gr-diff-line.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-ranged-comment-layer.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-ranged-comment-layer></gr-ranged-comment-layer>
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
index 79ff2a5..e9ac0a5 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-selection-action-box</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-selection-action-box.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<div>
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 0914846..808f689 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
@@ -78,7 +78,7 @@
};
var CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</;
- var CPP_WCHAR_PATTERN = /L\'.\'/g;
+ var CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g;
var JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g;
var GO_BACKSLASH_LITERAL = '\'\\\\\'';
var GLOBAL_LT_PATTERN = /</g;
@@ -360,7 +360,7 @@
* {#see https://github.com/isagalaev/highlight.js/issues/1412}
*/
if (CPP_WCHAR_PATTERN.test(line)) {
- line = line.replace(CPP_WCHAR_PATTERN, 'L"."');
+ line = line.replace(CPP_WCHAR_PATTERN, 'L"$1."');
}
return line;
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 096206f..e618d3a 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
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-syntax-layer</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
<link rel="import" href="gr-syntax-layer.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-syntax-layer></gr-syntax-layer>
@@ -454,6 +456,11 @@
line = 'wchar_t myChar = L\'#\'';
var expected = 'wchar_t myChar = L"."';
assert.equal(element._workaround('cpp', line), expected);
+
+ // Converts wchar_t character literal with escape sequence to string.
+ line = 'wchar_t myChar = L\'\\"\'';
+ expected = 'wchar_t myChar = L"\\."';
+ assert.equal(element._workaround('cpp', line), expected);
});
test('workaround go backslash character literals', function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html
index 13bea04..985fc6d 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html
@@ -18,11 +18,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-syntax-lib-loader</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="gr-syntax-lib-loader.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-syntax-lib-loader></gr-syntax-lib-loader>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index e78f77a..6811397 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -17,6 +17,7 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
<link rel="import" href="../styles/app-theme.html">
+<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
<link rel="import" href="./admin/gr-admin-view/gr-admin-view.html">
@@ -170,6 +171,9 @@
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
<gr-reporting id="reporting"></gr-reporting>
<gr-router id="router"></gr-router>
+ <gr-plugin-host id="plugins"
+ config="[[_serverConfig.plugin]]">
+ </gr-plugin-host>
</template>
<script src="gr-app.js" crossorigin="anonymous"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index 8b3592c..af24c5d 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -68,7 +68,6 @@
observers: [
'_viewChanged(params.view)',
- '_loadPlugins(_serverConfig.plugin.js_resource_paths)',
],
behaviors: [
@@ -120,7 +119,8 @@
// Preferences are cached when a user is logged in; warm them.
this.$.restAPI.getPreferences();
this.$.restAPI.getDiffPreferences();
- this.$.errorManager.knownAccountId = this._account._account_id;
+ this.$.errorManager.knownAccountId =
+ this._account && this._account._account_id || null;
},
_viewChanged: function(view) {
@@ -137,17 +137,6 @@
}
},
- _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);
- }
- },
-
_loginTapHandler: function(e) {
e.preventDefault();
page.show('/login/' + encodeURIComponent(
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index 9c709b0..5aacc77 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -18,11 +18,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-app</title>
-<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="gr-app.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-app id="app" canonical-path="/abc/def/ghi"></gr-app>
@@ -43,7 +45,8 @@
_getTopContent: sinon.stub(),
});
stub('gr-rest-api-interface', {
- getAccount: function() { return Promise.resolve(null); },
+ getAccount: function() { return Promise.resolve({}); },
+ getAccountCapabilities: function() { return Promise.resolve({}); },
getConfig: function() {
return Promise.resolve({
gerrit: {web_uis: ['GWT', 'POLYGERRIT']},
@@ -92,10 +95,12 @@
});
});
- test('sets plugins count', function() {
- sandbox.stub(Gerrit, '_setPluginsCount');
- element._loadPlugins([]);
- assert.isTrue(Gerrit._setPluginsCount.calledWithExactly(0));
+ test('passes config to gr-plugin-host', function(done) {
+ element.$.restAPI.getConfig.lastCall.returnValue.then(function(config) {
+ var pluginConfig = config.plugin;
+ assert.deepEqual(element.$.plugins.config, pluginConfig);
+ done();
+ });
});
test('canonical-path', function() {
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
new file mode 100644
index 0000000..a3c44e2
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
@@ -0,0 +1,22 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
+
+<dom-module id="gr-plugin-host">
+ <script src="gr-plugin-host.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
new file mode 100644
index 0000000..ccfe604
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -0,0 +1,52 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-plugin-host',
+
+ properties: {
+ config: {
+ type: Object,
+ observer: '_configChanged',
+ },
+ },
+
+ _configChanged: function(config) {
+ var jsPlugins = config.js_resource_paths || [];
+ var htmlPlugins = config.html_resource_paths || [];
+ Gerrit._setPluginsCount(jsPlugins.length + htmlPlugins.length);
+ this._loadJsPlugins(jsPlugins);
+ this._importHtmlPlugins(htmlPlugins);
+ },
+
+ _importHtmlPlugins: function(plugins) {
+ plugins.forEach(function(url) {
+ this.importHref('/' + url, null, Gerrit._pluginInstalled, true);
+ }.bind(this));
+ },
+
+ _loadJsPlugins: function(plugins) {
+ for (var i = 0; i < plugins.length; i++) {
+ var url = plugins[i];
+ var scriptEl = document.createElement('script');
+ scriptEl.defer = true;
+ scriptEl.src = '/' + plugins[i];
+ scriptEl.onerror = Gerrit._pluginInstalled;
+ document.body.appendChild(scriptEl);
+ }
+ },
+ });
+})();
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
new file mode 100644
index 0000000..b0c7c71
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-plugin-host</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="gr-plugin-host.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-plugin-host></gr-plugin-host>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff tests', function() {
+ var element;
+ var sandbox;
+
+ setup(function() {
+ element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ sandbox.stub(document.body, 'appendChild');
+ sandbox.stub(element, 'importHref');
+ });
+
+ teardown(function() {
+ sandbox.restore();
+ });
+
+ test('counts plugins', function() {
+ sandbox.stub(Gerrit, '_setPluginsCount');
+ element.config = {
+ html_resource_paths: ['foo/bar', 'baz'],
+ js_resource_paths: ['42'],
+ };
+ assert.isTrue(Gerrit._setPluginsCount.calledWith(3));
+ });
+
+ test('imports html plugins from config', function() {
+ element.config = {
+ html_resource_paths: ['foo/bar', 'baz'],
+ };
+ assert.isTrue(element.importHref.calledWith(
+ '/foo/bar', null, Gerrit._pluginInstalled, true));
+ assert.isTrue(element.importHref.calledWith(
+ '/baz', null, Gerrit._pluginInstalled, true));
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
index ffff0f1..8f4e89d 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-account-info</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-account-info.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-account-info></gr-account-info>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
index 13ecaee..d4443ac 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-table-editor.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-change-table-editor></gr-change-table-editor>
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
index 030b81c..b949643 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-email-editor</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-email-editor.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-email-editor></gr-email-editor>
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
index 56a476e..2abf797 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
@@ -18,11 +18,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="gr-group-list.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-group-list></gr-group-list>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
index 6a162a7..787c2c4 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-http-password.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-http-password></gr-http-password>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
index 74e9c6a..a7078093 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-menu-editor.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-menu-editor></gr-menu-editor>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
index 33f6aed..ee5a206 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-registration-dialog</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-registration-dialog.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-registration-dialog></gr-registration-dialog>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
index b3ce3b4..cb471d4 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-settings-view.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-settings-view></gr-settings-view>
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
index b248029..7bb5528 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-ssh-editor</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-ssh-editor.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-ssh-editor></gr-ssh-editor>
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
index 09f7381..59e87b0 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-settings-view</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-watched-projects-editor.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-watched-projects-editor></gr-watched-projects-editor>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
index 5f94658..de7d6a3 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -24,6 +24,8 @@
<link rel="import" href="gr-account-label.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-account-label></gr-account-label>
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 8c1af21..5cc0600 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
@@ -23,6 +23,8 @@
<link rel="import" href="gr-account-link.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-account-link></gr-account-link>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 9ebb794..e55ceb6 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -236,8 +236,11 @@
},
_cancel: function() {
- this._suggestions = [];
- this.fire('cancel');
+ if (this._suggestions.length) {
+ this._suggestions = [];
+ } else {
+ this.fire('cancel');
+ }
},
_updateValue: function(suggestions, index) {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
index 394b2c6..debff60 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reviewer-list</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-autocomplete.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-autocomplete></gr-autocomplete>
@@ -33,14 +35,20 @@
<script>
suite('gr-autocomplete tests', function() {
var element;
+ var sandbox;
setup(function() {
element = fixture('basic');
+ sandbox = sinon.sandbox.create();
+ });
+
+ teardown(function() {
+ sandbox.restore();
});
test('renders', function(done) {
var promise;
- var queryStub = sinon.spy(function(input) {
+ var queryStub = sandbox.spy(function(input) {
return promise = Promise.resolve([
{name: input + ' 0', value: 0},
{name: input + ' 1', value: 1},
@@ -74,9 +82,9 @@
});
});
- test('emits cancel', function(done) {
+ test('esc key behavior', function(done) {
var promise;
- var queryStub = sinon.spy(function() {
+ var queryStub = sandbox.spy(function() {
return promise = Promise.resolve([
{name: 'blah', value: 123},
]);
@@ -91,20 +99,23 @@
promise.then(function() {
assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
- var cancelHandler = sinon.spy();
+ var cancelHandler = sandbox.spy();
element.addEventListener('cancel', cancelHandler);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
- assert.isTrue(cancelHandler.called);
+ assert.isFalse(cancelHandler.called);
assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
+ assert.equal(element._suggestions.length, 0);
+ MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc');
+ assert.isTrue(cancelHandler.called);
done();
});
});
test('emits commit and handles cursor movement', function(done) {
var promise;
- var queryStub = sinon.spy(function(input) {
+ var queryStub = sandbox.spy(function(input) {
return promise = Promise.resolve([
{name: input + ' 0', value: 0},
{name: input + ' 1', value: 1},
@@ -123,7 +134,7 @@
promise.then(function() {
assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
- var commitHandler = sinon.spy();
+ var commitHandler = sandbox.spy();
element.addEventListener('commit', commitHandler);
assert.equal(element.$.cursor.index, 0);
@@ -156,14 +167,14 @@
test('clear-on-commit behavior (off)', function(done) {
var promise;
- var queryStub = sinon.spy(function() {
+ var queryStub = sandbox.spy(function() {
return promise = Promise.resolve([{name: 'suggestion', value: 0}]);
});
element.query = queryStub;
element.text = 'blah';
promise.then(function() {
- var commitHandler = sinon.spy();
+ var commitHandler = sandbox.spy();
element.addEventListener('commit', commitHandler);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
@@ -177,7 +188,7 @@
test('clear-on-commit behavior (on)', function(done) {
var promise;
- var queryStub = sinon.spy(function() {
+ var queryStub = sandbox.spy(function() {
return promise = Promise.resolve([{name: 'suggestion', value: 0}]);
});
element.query = queryStub;
@@ -185,7 +196,7 @@
element.clearOnCommit = true;
promise.then(function() {
- var commitHandler = sinon.spy();
+ var commitHandler = sandbox.spy();
element.addEventListener('commit', commitHandler);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
@@ -198,7 +209,7 @@
});
test('threshold guards the query', function() {
- var queryStub = sinon.spy(function() {
+ var queryStub = sandbox.spy(function() {
return Promise.resolve([]);
});
element.query = queryStub;
@@ -221,7 +232,7 @@
});
test('undefined or empty text results in no suggestions', function() {
- sinon.spy(element, '_updateSuggestions');
+ sandbox.spy(element, '_updateSuggestions');
element.text = undefined;
assert(element._updateSuggestions.calledOnce);
assert.equal(element._suggestions.length, 0);
@@ -229,14 +240,14 @@
test('multi completes only the last part of the query', function(done) {
var promise;
- var queryStub = sinon.stub()
+ var queryStub = sandbox.stub()
.returns(promise = Promise.resolve([{name: 'suggestion', value: 0}]));
element.query = queryStub;
element.text = 'blah blah';
element.multi = true;
promise.then(function() {
- var commitHandler = sinon.spy();
+ var commitHandler = sandbox.spy();
element.addEventListener('commit', commitHandler);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null,
@@ -249,18 +260,17 @@
});
test('tab key completes only when suggestions exist', function() {
- var commitStub = sinon.stub(element, '_commit');
+ var commitStub = sandbox.stub(element, '_commit');
element._suggestions = [];
MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
assert.isFalse(commitStub.called);
element._suggestions = ['tunnel snakes rule!'];
MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
assert.isTrue(commitStub.called);
- commitStub.restore();
});
test('tabCompleteWithoutCommit flag functions', function() {
- var commitHandler = sinon.spy();
+ var commitHandler = sandbox.spy();
element.addEventListener('commit', commitHandler);
element._suggestions = ['tunnel snakes rule!'];
element.tabCompleteWithoutCommit = true;
@@ -289,8 +299,8 @@
});
test('tap on suggestion commits and refocuses on input', function() {
- var focusSpy = sinon.spy(element, 'focus');
- var commitSpy = sinon.spy(element, '_commit');
+ var focusSpy = sandbox.spy(element, 'focus');
+ var commitSpy = sandbox.spy(element, '_commit');
element._focused = true;
element._suggestions = [{name: 'first suggestion'}];
assert.isFalse(element.$.suggestions.hasAttribute('hidden'));
@@ -300,12 +310,10 @@
assert.isTrue(commitSpy.called);
assert.isTrue(element.$.suggestions.hasAttribute('hidden'));
assert.isTrue(element._focused);
- focusSpy.restore();
- commitSpy.restore();
});
test('input-keydown event fired', function() {
- var listener = sinon.spy();
+ var listener = sandbox.spy();
element.addEventListener('input-keydown', listener);
MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab');
flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index 5ab27af..fd05d62 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -23,6 +23,8 @@
<link rel="import" href="gr-avatar.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-avatar></gr-avatar>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
index 70cf636..c269cb5 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-button</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-button.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-button></gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
index 969f7dd..460d860 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -24,6 +24,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-change-star.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-change-star></gr-change-star>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
index 812f32a..3eec979 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-confirm-dialog</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-confirm-dialog.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-confirm-dialog></gr-confirm-dialog>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
index d7496c7..5d9af80 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-cursor-manager</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-cursor-manager.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-cursor-manager cursor-target-class="targeted"></gr-cursor-manager>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index 5955cc5..4c6dfdf 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -24,6 +24,8 @@
<link rel="import" href="gr-date-formatter.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-date-formatter date-str="2015-09-24 23:30:17.033000000"></gr-date-formatter>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index 6ab1301..9b87f03 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -63,7 +63,11 @@
},
_handleDropdownTap: function(e) {
- this.$.dropdown.close();
+ // async is needed so that that the click event is fired before the
+ // dropdown closes (This was a bug for touch devices).
+ this.async(function() {
+ this.$.dropdown.close();
+ }, 1);
},
_showDropdownTapHandler: function(e) {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index 390213c..2db38b9 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -18,12 +18,14 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-dropdown</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-dropdown.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-dropdown></gr-dropdown>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
index 2515fe1..b25e815 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
@@ -24,6 +24,8 @@
<link rel="import" href="gr-editable-content.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-editable-content></gr-editable-content>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
index a87e5f6..42770aa 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -24,6 +24,8 @@
<link rel="import" href="gr-editable-label.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-editable-label
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
index b0054fc..5afe60a 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
@@ -23,6 +23,8 @@
<link rel="import" href="gr-formatted-text.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-formatted-text></gr-formatted-text>
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 cb9a7b8..20b5dcb 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
@@ -18,7 +18,7 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-actions-js-api</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
@@ -28,6 +28,8 @@
-->
<link rel="import" href="../../change/gr-change-actions/gr-change-actions.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-change-actions></gr-change-actions>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
index 304982c..d7d5cfe 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
@@ -18,16 +18,18 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-change-reply-js-api</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<!--
-This must refer to the element this interface is wrapping around. Otherwise
-breaking changes to gr-reply-dialog won’t be noticed.
--->
+ This must refer to the element this interface is wrapping around. Otherwise
+ breaking changes to gr-reply-dialog won’t be noticed.
+ -->
<link rel="import" href="../../change/gr-reply-dialog/gr-reply-dialog.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-reply-dialog></gr-reply-dialog>
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 13ee10e..d62bdd8 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
@@ -18,10 +18,12 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-api-interface</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="gr-js-api-interface.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-js-api-interface></gr-js-api-interface>
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 b3ae649..3a11605 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
@@ -107,7 +107,8 @@
}
// TODO(andybons): Polyfill currentScript for IE10/11 (edge supports it).
- var src = opt_src || (document.currentScript && document.currentScript.src);
+ var src = opt_src || (document.currentScript &&
+ document.currentScript.src || document.currentScript.baseURI);
var plugin = new Plugin(src);
try {
callback(plugin);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
index 5e2cac5..eefc79d 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
@@ -25,6 +25,8 @@
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-linked-chip.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-linked-chip></gr-linked-chip>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
index afb4a65..807278d 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -24,6 +24,8 @@
<link rel="import" href="gr-linked-text.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-linked-text>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index b0bc9bf..7cb29a1 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -971,9 +971,11 @@
// Sometimes the server doesn't send back the content type.
if (baseImage) {
baseImage._expectedType = diff.meta_a.content_type;
+ baseImage._name = diff.meta_a.name;
}
if (revisionImage) {
revisionImage._expectedType = diff.meta_b.content_type;
+ revisionImage._name = diff.meta_b.name;
}
return {baseImage: baseImage, revisionImage: revisionImage};
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index 2026183..31a98d9 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -18,13 +18,15 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-rest-api-interface</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<script src="../../../scripts/util.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
<link rel="import" href="gr-rest-api-interface.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-rest-api-interface></gr-rest-api-interface>
@@ -39,6 +41,13 @@
setup(function() {
sandbox = sinon.sandbox.create();
element = fixture('basic');
+ var testJSON = ')]}\'\n{"hello": "bonjour"}';
+ sandbox.stub(window, 'fetch').returns(Promise.resolve({
+ ok: true,
+ text: function() {
+ return Promise.resolve(testJSON);
+ },
+ }));
});
teardown(function() {
@@ -46,16 +55,6 @@
});
test('JSON prefix is properly removed', function(done) {
- var testJSON = ')]}\'\n{"hello": "bonjour"}';
-
- sandbox.stub(window, 'fetch', function() {
- return Promise.resolve({
- ok: true,
- text: function() {
- return Promise.resolve(testJSON);
- },
- });
- });
element.fetchJSON('/dummy/url').then(function(obj) {
assert.deepEqual(obj, {hello: 'bonjour'});
done();
@@ -113,13 +112,11 @@
test('request callbacks can be canceled', function(done) {
var cancelCalled = false;
- sandbox.stub(window, 'fetch', function() {
- return Promise.resolve({
- body: {
- cancel: function() { cancelCalled = true; },
- },
- });
- });
+ window.fetch.returns(Promise.resolve({
+ body: {
+ cancel: function() { cancelCalled = true; },
+ },
+ }));
element.fetchJSON('/dummy/url', null, function() { return true; }).then(
function(obj) {
assert.isUndefined(obj);
@@ -416,9 +413,7 @@
test('server error', function(done) {
var getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
- sandbox.stub(window, 'fetch', function() {
- return Promise.resolve({ok: false});
- });
+ window.fetch.returns(Promise.resolve({ok: false}));
var serverErrorEventPromise = new Promise(function(resolve) {
element.addEventListener('server-error', function() { resolve(); });
});
@@ -446,6 +441,7 @@
text: function() { return Promise.resolve(')]}\'{}'); },
},
];
+ window.fetch.restore();
sandbox.stub(window, 'fetch', function(url) {
if (url === '/accounts/self/detail') {
return Promise.resolve(responses.shift());
@@ -563,13 +559,12 @@
'PUT', '/config/server/email.confirm', {token: 'foo'}));
});
- test('GrReviewerUpdatesParser.parse is used', function(done) {
+ test('GrReviewerUpdatesParser.parse is used', function() {
sandbox.stub(GrReviewerUpdatesParser, 'parse').returns(
Promise.resolve('foo'));
- element.getChangeDetail(42).then(function(result) {
+ return element.getChangeDetail(42).then(function(result) {
assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce);
assert.equal(result, 'foo');
- done();
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
index 7da5f74..1ae04a0 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -18,7 +18,7 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-reviewer-updates-parser</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
index abdff64..bd22505 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
@@ -18,11 +18,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-select</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="gr-select.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<select is="gr-select">
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
index b17a9c9..6d77c55 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -17,11 +17,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-storage</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="gr-storage.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-storage></gr-storage>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
index 7925c91..aac2ea8 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
@@ -17,11 +17,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-storage</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="gr-tooltip-content.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-tooltip-content>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
index 9e46aa7..69a5b75 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
@@ -17,11 +17,13 @@
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-storage</title>
-<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="gr-tooltip.html">
+<script>void(0);</script>
+
<test-fixture id="basic">
<template>
<gr-tooltip>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index a593c8d..ca50bbc 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -37,9 +37,9 @@
'change/gr-change-view/gr-change-view_test.html',
'change/gr-comment-list/gr-comment-list_test.html',
'change/gr-commit-info/gr-commit-info_test.html',
+ 'change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html',
'change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
'change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html',
- 'change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html',
'change/gr-download-dialog/gr-download-dialog_test.html',
'change/gr-file-list/gr-file-list_test.html',
'change/gr-message/gr-message_test.html',
@@ -53,8 +53,8 @@
'core/gr-reporting/gr-reporting_test.html',
'core/gr-search-bar/gr-search-bar_test.html',
'diff/gr-diff-builder/gr-diff-builder_test.html',
- 'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
'diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html',
+ 'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
'diff/gr-diff-comment/gr-diff-comment_test.html',
'diff/gr-diff-cursor/gr-diff-cursor_test.html',
'diff/gr-diff-highlight/gr-annotation_test.html',
@@ -71,6 +71,7 @@
'diff/gr-syntax-layer/gr-syntax-layer_test.html',
'diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html',
'gr-app_test.html',
+ 'plugins/gr-plugin-host/gr-plugin-host_test.html',
'settings/gr-account-info/gr-account-info_test.html',
'settings/gr-change-table-editor/gr-change-table-editor_test.html',
'settings/gr-email-editor/gr-email-editor_test.html',
diff --git a/tools/version.py b/tools/version.py
index fee1477..fed6d5d 100755
--- a/tools/version.py
+++ b/tools/version.py
@@ -51,9 +51,5 @@
pom = os.path.join(project, 'pom.xml')
replace_in_file(pom, src_pattern)
-src_pattern = re.compile(r"^(GERRIT_VERSION = ')([-.\w]+)(')$", re.MULTILINE)
+src_pattern = re.compile(r'^(GERRIT_VERSION = ")([-.\w]+)(")$', re.MULTILINE)
replace_in_file('version.bzl', src_pattern)
-
-src_pattern = re.compile(r'^(\s*-DarchetypeVersion=)([-.\w]+)(\s*\\)$',
- re.MULTILINE)
-replace_in_file(os.path.join('Documentation', 'dev-plugins.txt'), src_pattern)
diff --git a/version.bzl b/version.bzl
index 24a3c27..488a83f 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
# Used by :api_install and :api_deploy targets
# when talking to the destination repository.
#
-GERRIT_VERSION = "2.14-SNAPSHOT"
+GERRIT_VERSION = "2.15-SNAPSHOT"