Merge "Improving message when needing to reindex"
diff --git a/.buckconfig b/.buckconfig
index c766d30..7814eec 100644
--- a/.buckconfig
+++ b/.buckconfig
@@ -21,10 +21,12 @@
includes = //tools/default.defs
[java]
+ jar_spool_mode = direct_to_jar
src_roots = java, resources
[project]
ignore = .git, eclipse-out
+ parallel_parsing = true
[cache]
mode = dir
diff --git a/.buckversion b/.buckversion
index b2b427a..c103f73 100644
--- a/.buckversion
+++ b/.buckversion
@@ -1 +1 @@
-75b74ccf90a590b284b1a1553dc48af8844a9ca7
+01a0c54d8271df6d4084750340c1035e497775e0
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 70a695e..0590337 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -128,6 +128,12 @@
or invalid value) and votes that are not permitted for the user are
silently ignored.
+--strict-labels::
+ Require ability to vote on all specified labels before reviewing change.
+ If the vote is invalid (invalid label or invalid name), the vote is not
+ permitted for the user, or the vote is on an outdated or closed patch set,
+ return an error instead of silently discarding the vote.
+
== ACCESS
Any user who has configured an SSH key.
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index fb5bc15..df9ec26 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -145,7 +145,7 @@
The configured <<ldap.username,ldap.username>> identity is not used to obtain
account information.
+
-* OAUTH
+* `OAUTH`
+
OAuth is a protocol that lets external apps request authorization to private
details in a user's account without getting their password. This is
@@ -437,9 +437,9 @@
[[auth.gitBasicAuth]]auth.gitBasicAuth::
+
If true then Git over HTTP and HTTP/S traffic is authenticated using
-standard BasicAuth and the credentials are validated against the randomly
-generated HTTP password or against LDAP when it is configured as Gerrit
-Web UI authentication method.
+standard BasicAuth. Depending on the configured `auth.type` credentials
+are validated against the randomly generated HTTP password, against LDAP
+(`auth.type = LDAP`) or against an OAuth 2 provider (`auth.type = OAUTH`).
+
This parameter affects git over HTTP traffic and access to the REST
API. If set to false then Gerrit will authenticate through DIGEST
@@ -449,8 +449,30 @@
When `auth.type` is `LDAP`, service users that only exist in the Gerrit
database are still authenticated by their HTTP passwords.
+
+When `auth.type` is `OAUTH`, Git clients may send OAuth 2 access tokens
+instead of passwords in the Basic authentication header. Note that provider
+specific plugins must be installed to facilitate this authentication scheme.
+If multiple OAuth 2 provider plugins are installed one of them must be
+selected as default with the `auth.gitOAuthProvider` option.
++
By default this is set to false.
+[[auth.gitOAuthProvider]]auth.gitOAuthProvider::
++
+Selects the OAuth 2 provider to authenticate git over HTTP traffic with.
++
+In general there is no way to determine from an access token alone, which
+OAuth 2 provider to address to verify that token, and the BasicAuth
+scheme does not support amending such details. If multiple OAuth provider
+plugins in a system offer support for git over HTTP authentication site
+administrators must configure, which one to use as default provider.
+In case the provider cannot be determined from a request the access token
+will be sent to the default provider for verification.
++
+The value of this parameter must be the identifier of an OAuth 2 provider
+in the form `plugin-name:provider-name`. Consult the respective plugin
+documentation for details.
+
[[auth.userNameToLowerCase]]auth.userNameToLowerCase::
+
If set the username that is received to authenticate a git operation
@@ -2539,6 +2561,9 @@
+
Root of the tree containing all user accounts. This is typically
of the form `ou=people,dc=example,dc=com`.
++
+This setting may be added multiple times to specify more than
+one root.
[[ldap.accountScope]]ldap.accountScope::
+
@@ -2650,6 +2675,9 @@
+
Root of the tree containing all group objects. This is typically
of the form `ou=groups,dc=example,dc=com`.
++
+This setting may be added multiple times to specify more than
+one root.
[[ldap.groupScope]]ldap.groupScope::
+
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index b53da4b..d68dc8a 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -48,6 +48,13 @@
Prolog based submit type computes a submit type for each change. The computed
submit type is shown on the change screen for each change.
+When submitting changes in a batch using "Submit including ancestors" or "Submit
+whole topic", submit type rules may not be used to mix submit types on a single
+branch, and trying to submit such a batch will fail. This avoids potentially
+confusing behavior and spurious submit failures. It is recommended to only use
+submit type rules to change submit types for an entire branch, which avoids this
+situation.
+
== Prolog Language
This document is not a complete Prolog tutorial.
link:http://en.wikipedia.org/wiki/Prolog[This Wikipedia page on Prolog] is a
@@ -999,37 +1006,13 @@
submit_type(fast_forward_only) :-
gerrit:change_branch(B), regex_matches('refs/heads/stable.*', B),
!.
-submit_type(T) :- gerrit:project_default_submit_type(T)
+submit_type(T) :- gerrit:project_default_submit_type(T).
----
The first `submit_type` predicate defines the `Fast Forward Only` submit type
for `+refs/heads/stable.*+` branches. The second `submit_type` predicate returns
the project's default submit type.
-=== Example 3: Don't require `Fast Forward Only` if only documentation was changed
-Like in the previous example we want the `Fast Forward Only` submit type for the
-`+refs/heads/stable*+` branches. However, if only documentation was changed
-(only `+*.txt+` files), then we allow project's default submit type for such
-changes.
-
-`rules.pl`
-[source,prolog]
-----
-submit_type(fast_forward_only) :-
- gerrit:commit_delta('(?<!\.txt)$'),
- gerrit:change_branch(B), regex_matches('refs/heads/stable.*', B),
- !.
-submit_type(T) :- gerrit:project_default_submit_type(T)
-----
-
-The `gerrit:commit_delta('(?<!\.txt)$')` succeeds if the change contains a file
-whose name doesn't end with `.txt` The rest of this rule is same like in the
-previous example.
-
-If all file names in the change end with `.txt`, then the
-`gerrit:commit_delta('(?<!\.txt)$')` will fail as no file name will match this
-regular expression.
-
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 07d3cc3..20b1dab 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1681,9 +1681,6 @@
[[edit-endpoints]]
== Change Edit Endpoints
-These endpoints are considered to be unstable and can be changed in
-backwards incompatible way any time without notice.
-
[[get-edit-detail]]
=== Get Change Edit Details
--
@@ -3953,6 +3950,9 @@
|`reviewed` |not set if `false`|
Whether the change was reviewed by the calling user.
Only set if link:#reviewed[reviewed] is requested.
+|`submit_type` |optional|
+The link:project-configuration.html#submit_type[submit type] of the change. +
+Not set for merged changes.
|`mergeable` |optional|
Whether the change is mergeable. +
Not set for merged changes, or if the change has not yet been tested.
diff --git a/ReleaseNotes/ReleaseNotes-2.11.5.txt b/ReleaseNotes/ReleaseNotes-2.11.5.txt
index 03427bb..d7758cb 100644
--- a/ReleaseNotes/ReleaseNotes-2.11.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.11.5.txt
@@ -9,6 +9,22 @@
There are no schema changes from link:ReleaseNotes-2.11.4.html[2.11.4].
+Important Notes
+---------------
+
+*WARNING:* This release uses a forked version of buck.
+
+Buck was forked to cherry-pick an upstream fix for building on Mac OSX
+El Capitan.
+
+To build this release from source, the Google repository must be added to
+the remotes in the buck checkout:
+
+----
+ $ git remote add google https://gerrit.googlesource.com/buck
+----
+
+
Bug Fixes
---------
diff --git a/ReleaseNotes/ReleaseNotes-2.12.txt b/ReleaseNotes/ReleaseNotes-2.12.txt
index 72ff927..d6c33f61 100644
--- a/ReleaseNotes/ReleaseNotes-2.12.txt
+++ b/ReleaseNotes/ReleaseNotes-2.12.txt
@@ -15,7 +15,7 @@
java -jar gerrit.war init -d site_path
----
-*WARNING:* To use online reindexing when upgrading to 2.12.x the server must
+*WARNING:* To use online reindexing when upgrading to 2.12.x, the server must
first be upgraded to 2.8 (or 2.9) and then through 2.10 and 2.11 to 2.12.x. If
reindexing will be done offline, you may ignore this warning and upgrade directly
to 2.12.x.
@@ -24,11 +24,20 @@
Bouncy Castle Crypto, new versions of the libraries will be downloaded. The old
libraries should be manually removed from site's `lib` folder to prevent the
startup failure described in
-link:https://code.google.com/p/gerrit/issues/detail?id=3084[Issue 3084].
+link:https://code.google.com/p/gerrit/issues/detail?id=3084[issue 3084].
*WARNING:* The Solr secondary index is no longer supported. With this release
the only supported secondary index is Lucene.
+*WARNING:* The format of the `ref-updated` event has changed. Users of the
+link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[
+Jenkins Gerrit Trigger plugin] with jobs triggering on `ref-updated` should
+upgrade to at least
+link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger#GerritTrigger-Version2.15.1%28releasedSept142015%29[
+version 2.15.1]. If an upgrade of the plugin is not possible, a workaround is
+to change the branch configuration to type `Path` with a pattern like
+`refs/*/master` instead of `Plain` and `master`.
+
Release Highlights
------------------
@@ -36,7 +45,7 @@
This release includes the following new features. See the sections below for
further details.
-* New change submission workflows, "Submit Whole Topic" and "Submitted Together".
+* New change submission workflows: 'Submit Whole Topic' and 'Submitted Together'.
* Support for GPG Keys and signed pushes.
@@ -47,7 +56,7 @@
New Change Submission Workflows
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-* New "Submit Whole Topic" setting.
+* New 'Submit Whole Topic' setting.
+
When the
link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#change.submitWholeTopic[
@@ -64,7 +73,7 @@
* The merge queue is removed.
+
Changes that cannot be submitted due to missing dependencies will no longer
-enter the "Submitted, Merge Pending" state.
+enter the 'Submitted, Merge Pending' state.
GPG Keys and Signed Pushes
@@ -88,7 +97,7 @@
* Administrators may also configure
link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.certNonceSeed[
-`receive.certNonceSeed`]
+`receive.certNonceSeed`] and
link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/config-gerrit.html#receive.certNonceSlop[
`receive.certNonceSlop`].
@@ -107,14 +116,14 @@
* Add `from:` search operator to match by owner of change or author of comments.
-* Add `commentby:` search operator to search byt author of comments.
+* Add `commentby:` search operator to search by author of comments.
* Change the `topic:` search operator to search by the exact topic name.
* Add `intopic:` search operator to search by topics containing the search term.
* link:http://code.google.com/p/gerrit/issues/detail?id=3291[Issue 3291]:
-Add `has:edit` search operator to match changes that have change edits on them.
+Add `has:edit` search operator to match changes that have edit revisions on them.
* Allow configuration of maximum query size.
+
@@ -123,8 +132,8 @@
* Expose Lucene index writers for plugins.
+
-Plugins can now be written to allow runtime reconfiguration of various Lucene
-performance related parameters.
+Plugins can now reconfigure various Lucene performance related parameters
+at runtime.
* Make Lucene index writers auto-commit writers.
+
@@ -274,7 +283,7 @@
Set Review]: Add an option to omit duplicate comments.
* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#get-safe-content[
-Download Content]: Downloads the content of a file from a certain revision, in a
+Download Content]: Download the content of a file from a certain revision, in a
safe format that poses no risk for inadvertent execution of untrusted code.
* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-changes.html#submitted-together[
@@ -289,7 +298,7 @@
^^^^^^
* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-config.html#get-info[
-Get Server Info]: Returns information about the Gerrit server configuration.
+Get Server Info]: Return information about the Gerrit server configuration.
* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-config.html#confirm-email[
Confirm Email]: Confirm that the user owns an email address.
@@ -304,7 +313,7 @@
This allows group auto-completion to be used in a plugin's UI.
* link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/rest-api-groups.html#get-audit-log[
-Get Audit Log]: Gets the audit log of a Gerrit internal group, showing member
+Get Audit Log]: Get the audit log of a Gerrit internal group, showing member
additions, removals, and the user who made the change.
@@ -444,7 +453,7 @@
link:https://tools.ietf.org/html/rfc1035#section-3.1[RFC 1035 section 3.1].
And in practice, even the local-part is typically case insensitive also.
-* commit-msg hook: Don't add Change-Id line on temporary commits.
+* `commit-msg` hook: Don't add `Change-Id` line on temporary commits.
+
Commits created with `git commit --fixup` or `git commit --squash` are not
intended to be pushed to Gerrit, and don't need a `Change-Id` line.
@@ -456,12 +465,20 @@
download-commands plugin: Fix clone with commit-msg hook when project name
contains '/'.
-* Include full ref names in `ref-updated` events.
+* Use full ref name in `refName` attribute of `ref-updated` events.
+
The link:https://gerrit-documentation.storage.googleapis.com/Documentation/2.12/json.html#refUpdate[
refUpdate attribute] in `ref-updated` events did not include the full name
-of the ref, i.e. `master` instead of `refs/heads/master`.
-
+of the ref in the `refName` attribute, i.e. `master` was used instead of
+`refs/heads/master`.
++
+Support for the new format is added in
+link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger#GerritTrigger-Version2.15.1%28releasedSept142015%29[
+version 2.15.1 of the Jenkins Gerrit Trigger plugin].
++
+Users who are unable to upgrade the plugin may instead change the
+trigger's branch configuration to type `Path` with a pattern like
+`refs/*/master` instead of `Plain` and `master`.
Upgrades
--------
diff --git a/ReleaseNotes/ReleaseNotes-2.13.txt b/ReleaseNotes/ReleaseNotes-2.13.txt
new file mode 100644
index 0000000..aba6131
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.13.txt
@@ -0,0 +1,85 @@
+Release notes for Gerrit 2.13
+=============================
+
+
+Gerrit 2.13 is now available:
+
+link:https://www.gerritcodereview.com/download/gerrit-2.13.war[
+https://www.gerritcodereview.com/download/gerrit-2.13.war]
+
+
+Important Notes
+---------------
+
+TODO
+
+
+Release Highlights
+------------------
+
+* Metrics interface.
+
+
+New Features
+------------
+
+Metrics
+~~~~~~~
+
+Metrics about Gerrit's internal state can be sent to external
+monitoring systems.
+
+The following metrics are supported:
+
+* HTTP responses
++
+TODO details here and in the others
+
+* REST API calls
+
+* SSH sessions
+
+* Caches
+
+* SQL connections
+
+* TODO add more
+
+
+Plugins can provide implementations of the metrics interface to
+report metrics to different monitoring systems. The following
+plugins are available:
+
+* JMX
+
+* Graphite
+
+* Elasticsearch
+
+
+Plugins can also provide metrics. The following metrics are provided
+by core plugins:
+
+* Replication
+
+** Replication time
+
+* TODO add more
+
+Changes
+~~~~~~~
+
+In order to avoid potentially confusing behavior, when submitting changes in a
+batch, submit type rules may not be used to mix submit types on a single branch,
+and trying to submit such a batch will fail.
+
+Bug Fixes
+---------
+
+TODO
+
+
+Upgrades
+--------
+
+* Upgrade Lucene to 5.3.1
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index e61d476..e15a845 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,11 @@
Gerrit Code Review - Release Notes
==================================
+[[2_13]]
+Version 2.13.x
+--------------
+* link:ReleaseNotes-2.13.html[2.13]
+
[[2_12]]
Version 2.12.x
--------------
diff --git a/bucklets/gerrit_plugin.bucklet b/bucklets/gerrit_plugin.bucklet
index cd2edae..367fe71 100644
--- a/bucklets/gerrit_plugin.bucklet
+++ b/bucklets/gerrit_plugin.bucklet
@@ -14,7 +14,7 @@
# When compiling from standalone cookbook-plugin, bucklets directory points
# to cloned bucklets library that includes real gerrit_plugin.bucklet code.
-GERRIT_GWT_API = ['//gerrit-plugin-gwtui/gerrit:gwtui-api']
+GERRIT_GWT_API = ['//gerrit-plugin-gwtui:gwtui-api']
GERRIT_PLUGIN_API = ['//gerrit-plugin-api:lib']
GERRIT_TESTS = ['//gerrit-acceptance-framework:lib']
diff --git a/bucklets/local_jar.bucklet b/bucklets/local_jar.bucklet
deleted file mode 120000
index 8904824..0000000
--- a/bucklets/local_jar.bucklet
+++ /dev/null
@@ -1 +0,0 @@
-../lib/local.defs
\ No newline at end of file
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 3cea4a2..f2c6ac3 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
@@ -519,20 +519,22 @@
protected void setUseContributorAgreements(InheritableBoolean value)
throws Exception {
- MetaDataUpdate md = metaDataUpdateFactory.create(project);
- ProjectConfig config = ProjectConfig.read(md);
- config.getProject().setUseContributorAgreements(value);
- config.commit(md);
- projectCache.evict(config.getProject());
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+ ProjectConfig config = ProjectConfig.read(md);
+ config.getProject().setUseContributorAgreements(value);
+ config.commit(md);
+ projectCache.evict(config.getProject());
+ }
}
protected void setUseSignedOffBy(InheritableBoolean value)
throws Exception {
- MetaDataUpdate md = metaDataUpdateFactory.create(project);
- ProjectConfig config = ProjectConfig.read(md);
- config.getProject().setUseSignedOffBy(value);
- config.commit(md);
- projectCache.evict(config.getProject());
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+ ProjectConfig config = ProjectConfig.read(md);
+ config.getProject().setUseSignedOffBy(value);
+ config.commit(md);
+ projectCache.evict(config.getProject());
+ }
}
protected void deny(String permission, AccountGroup.UUID id, String ref)
@@ -552,11 +554,8 @@
protected void saveProjectConfig(Project.NameKey p, ProjectConfig cfg)
throws Exception {
- MetaDataUpdate md = metaDataUpdateFactory.create(p);
- try {
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(p)) {
cfg.commit(md);
- } finally {
- md.close();
}
projectCache.evict(cfg.getProject());
}
@@ -569,17 +568,18 @@
protected void grant(String permission, Project.NameKey project, String ref,
boolean force) throws RepositoryNotFoundException, IOException,
ConfigInvalidException {
- MetaDataUpdate md = metaDataUpdateFactory.create(project);
- md.setMessage(String.format("Grant %s on %s", permission, ref));
- ProjectConfig config = ProjectConfig.read(md);
- AccessSection s = config.getAccessSection(ref, true);
- Permission p = s.getPermission(permission, true);
- AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
- PermissionRule rule = new PermissionRule(config.resolve(adminGroup));
- rule.setForce(force);
- p.add(rule);
- config.commit(md);
- projectCache.evict(config.getProject());
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+ md.setMessage(String.format("Grant %s on %s", permission, ref));
+ ProjectConfig config = ProjectConfig.read(md);
+ AccessSection s = config.getAccessSection(ref, true);
+ Permission p = s.getPermission(permission, true);
+ AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+ PermissionRule rule = new PermissionRule(config.resolve(adminGroup));
+ rule.setForce(force);
+ p.add(rule);
+ config.commit(md);
+ projectCache.evict(config.getProject());
+ }
}
protected void blockRead(String ref) throws Exception {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
index 4c77edc..c71e13f 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PluginDaemonTest.java
@@ -41,6 +41,7 @@
private static final String BUCKLC = "buck";
private static final String BUCKOUT = "buck-out";
+ private static final String ECLIPSE = "eclipse-out";
private Path gen;
private Path pluginRoot;
@@ -98,7 +99,7 @@
if (subPath.endsWith("plugins")) {
pluginsIdx = idx;
}
- if (subPath.endsWith(BUCKOUT)) {
+ if (subPath.endsWith(BUCKOUT) || subPath.endsWith(ECLIPSE)) {
buckOutIdx = idx;
}
idx++;
@@ -126,7 +127,8 @@
String gerritDirCandidate =
partialPath.subpath(count - 2, count - 1).toString();
if (pattern.matcher(gerritDirCandidate).matches()) {
- if (partialPath.endsWith(gerritDirCandidate + "/" + BUCKOUT)) {
+ if (partialPath.endsWith(gerritDirCandidate + "/" + BUCKOUT) ||
+ partialPath.endsWith(gerritDirCandidate + "/" + ECLIPSE)) {
return false;
}
}
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 39296d0..37d5484 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
@@ -141,6 +141,19 @@
}
@Test
+ public void getByIntId() throws Exception {
+ AccountInfo info = gApi
+ .accounts()
+ .id("admin")
+ .get();
+ AccountInfo infoByIntId = gApi
+ .accounts()
+ .id(info._accountId)
+ .get();
+ assertThat(info.name).isEqualTo(infoByIntId.name);
+ }
+
+ @Test
public void self() throws Exception {
AccountInfo info = gApi
.accounts()
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 9be1865..8e001e6 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
@@ -21,6 +21,7 @@
import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER;
import static com.google.gerrit.server.project.Util.blockLabel;
import static com.google.gerrit.server.project.Util.category;
import static com.google.gerrit.server.project.Util.value;
@@ -42,6 +43,7 @@
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.ListChangesOption;
+import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -84,6 +86,7 @@
assertThat(c.branch).isEqualTo("master");
assertThat(c.status).isEqualTo(ChangeStatus.NEW);
assertThat(c.subject).isEqualTo("test commit");
+ assertThat(c.submitType).isEqualTo(SubmitType.MERGE_IF_NECESSARY);
assertThat(c.mergeable).isTrue();
assertThat(c.changeId).isEqualTo(r.getChangeId());
assertThat(c.created).isEqualTo(c.updated);
@@ -240,6 +243,37 @@
assertThat(committer.email).isEqualTo(admin.email);
}
+ @Test
+ public void voteOnBehalfOf() throws Exception {
+ ProjectConfig cfg = projectCache.checkedGet(project).getConfig();
+ LabelType codeReviewType = Util.codeReview();
+ String forCodeReviewAs = Permission.forLabelAs(codeReviewType.getName());
+ String heads = "refs/heads/*";
+ AccountGroup.UUID owner =
+ SystemGroupBackend.getGroup(CHANGE_OWNER).getUUID();
+ Util.allow(cfg, forCodeReviewAs, -1, 1, owner, heads);
+ saveProjectConfig(project, cfg);
+
+ PushOneCommit.Result r = createChange();
+ RevisionApi revision = gApi.changes()
+ .id(r.getChangeId())
+ .current();
+
+ ReviewInput in = ReviewInput.recommend();
+ in.onBehalfOf = user.id.toString();
+ revision.review(in);
+
+ ChangeInfo c = gApi.changes()
+ .id(r.getChangeId())
+ .get();
+
+ LabelInfo codeReview = c.labels.get("Code-Review");
+ assertThat(codeReview.all).hasSize(1);
+ ApprovalInfo approval = codeReview.all.get(0);
+ assertThat(approval._accountId).isEqualTo(user.id.get());
+ assertThat(approval.value).isEqualTo(1);
+ }
+
@Test(expected = ResourceConflictException.class)
public void rebaseUpToDateChange() throws Exception {
PushOneCommit.Result r = createChange();
@@ -457,12 +491,12 @@
setApiUser(admin);
gApi.changes()
.id(r.getChangeId())
- .reviewer(admin.getId().toString())
+ .reviewer(user.getId().toString())
.deleteVote("Code-Review");
Map<String, Short> m = gApi.changes()
.id(r.getChangeId())
- .reviewer(admin.getId().toString())
+ .reviewer(user.getId().toString())
.votes();
if (isNoteDbTestEnabled()) {
@@ -482,8 +516,10 @@
.id(r.getChangeId())
.get();
- assertThat(Iterables.getLast(c.messages).message).isEqualTo(
- "Removed Code-Review+2 by Administrator <admin@example.com>\n");
+ ChangeMessageInfo message = Iterables.getLast(c.messages);
+ assertThat(message.author._accountId).isEqualTo(admin.getId().get());
+ assertThat(message.message).isEqualTo(
+ "Removed Code-Review+1 by User <user@example.com>\n");
if (isNoteDbTestEnabled()) {
// When notedb is enabled each reviewer is explicitly recorded in the
// notedb and this record stays even when all votes of that user have been
@@ -495,9 +531,9 @@
// When notedb is disabled users that have only dummy 0 approvals on the
// change are returned as CC and not as REVIEWER.
assertThat(getReviewers(c.reviewers.get(REVIEWER)))
- .containsExactlyElementsIn(ImmutableSet.of(user.getId()));
- assertThat(getReviewers(c.reviewers.get(CC)))
.containsExactlyElementsIn(ImmutableSet.of(admin.getId()));
+ assertThat(getReviewers(c.reviewers.get(CC)))
+ .containsExactlyElementsIn(ImmutableSet.of(user.getId()));
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
new file mode 100644
index 0000000..1033164
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
@@ -0,0 +1,261 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.api.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.extensions.client.SubmitType.CHERRY_PICK;
+import static com.google.gerrit.extensions.client.SubmitType.FAST_FORWARD_ONLY;
+import static com.google.gerrit.extensions.client.SubmitType.MERGE_ALWAYS;
+import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY;
+import static com.google.gerrit.extensions.client.SubmitType.REBASE_IF_NECESSARY;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.ImmutableList;
+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.ReviewInput;
+import com.google.gerrit.extensions.api.projects.BranchInput;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.VersionedMetaData;
+import com.google.gerrit.testutil.ConfigSuite;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@NoHttpd
+public class SubmitTypeRuleIT extends AbstractDaemonTest {
+ @ConfigSuite.Default
+ public static Config submitWholeTopicEnabled() {
+ return submitWholeTopicEnabledConfig();
+ }
+
+ private class RulesPl extends VersionedMetaData {
+ private static final String FILENAME = "rules.pl";
+
+ private String rule;
+
+ @Override
+ protected String getRefName() {
+ return RefNames.REFS_CONFIG;
+ }
+
+ @Override
+ protected void onLoad() throws IOException, ConfigInvalidException {
+ rule = readUTF8(FILENAME);
+ }
+
+ @Override
+ protected boolean onSave(CommitBuilder commit)
+ throws IOException, ConfigInvalidException {
+ TestSubmitRuleInput in = new TestSubmitRuleInput();
+ in.rule = rule;
+ try {
+ gApi.changes().id(testChangeId.get()).current().testSubmitType(in);
+ } catch (RestApiException e) {
+ throw new ConfigInvalidException("Invalid submit type rule", e);
+ }
+
+ saveUTF8(FILENAME, rule);
+ return true;
+ }
+ }
+
+ private AtomicInteger fileCounter;
+ private Change.Id testChangeId;
+
+ @Before
+ public void setUp() throws Exception {
+ fileCounter = new AtomicInteger();
+ gApi.projects().name(project.get()).branch("test")
+ .create(new BranchInput());
+ testChangeId = createChange("test", "test change").getChange().getId();
+ }
+
+ private void setRulesPl(String rule) throws Exception {
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+ RulesPl r = new RulesPl();
+ r.load(md);
+ r.rule = rule;
+ r.commit(md);
+ }
+ }
+
+ private static final String SUBMIT_TYPE_FROM_SUBJECT =
+ "submit_type(fast_forward_only) :-"
+ + "gerrit:commit_message(M),"
+ + "regex_matches('.*FAST_FORWARD_ONLY.*', M),"
+ + "!.\n"
+ + "submit_type(merge_if_necessary) :-"
+ + "gerrit:commit_message(M),"
+ + "regex_matches('.*MERGE_IF_NECESSARY.*', M),"
+ + "!.\n"
+ + "submit_type(rebase_if_necessary) :-"
+ + "gerrit:commit_message(M),"
+ + "regex_matches('.*REBASE_IF_NECESSARY.*', M),"
+ + "!.\n"
+ + "submit_type(merge_always) :-"
+ + "gerrit:commit_message(M),"
+ + "regex_matches('.*MERGE_ALWAYS.*', M),"
+ + "!.\n"
+ + "submit_type(cherry_pick) :-"
+ + "gerrit:commit_message(M),"
+ + "regex_matches('.*CHERRY_PICK.*', M),"
+ + "!.\n"
+ + "submit_type(T) :- gerrit:project_default_submit_type(T).";
+
+ private PushOneCommit.Result createChange(String dest, String subject)
+ throws Exception {
+ PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+ subject, "file" + fileCounter.incrementAndGet(),
+ PushOneCommit.FILE_CONTENT);
+ PushOneCommit.Result r = push.to("refs/for/" + dest);
+ r.assertOkStatus();
+ return r;
+ }
+
+ @Test
+ public void unconditionalCherryPick() throws Exception {
+ PushOneCommit.Result r = createChange();
+ assertSubmitType(MERGE_IF_NECESSARY, r.getChangeId());
+ setRulesPl("submit_type(cherry_pick).");
+ assertSubmitType(CHERRY_PICK, r.getChangeId());
+ }
+
+ @Test
+ public void submitTypeFromSubject() throws Exception {
+ PushOneCommit.Result r1 = createChange("master", "Default 1");
+ PushOneCommit.Result r2 = createChange("master", "FAST_FORWARD_ONLY 2");
+ PushOneCommit.Result r3 = createChange("master", "MERGE_IF_NECESSARY 3");
+ PushOneCommit.Result r4 = createChange("master", "REBASE_IF_NECESSARY 4");
+ PushOneCommit.Result r5 = createChange("master", "MERGE_ALWAYS 5");
+ PushOneCommit.Result r6 = createChange("master", "CHERRY_PICK 6");
+
+ assertSubmitType(MERGE_IF_NECESSARY, r1.getChangeId());
+ assertSubmitType(MERGE_IF_NECESSARY, r2.getChangeId());
+ assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId());
+ assertSubmitType(MERGE_IF_NECESSARY, r4.getChangeId());
+ assertSubmitType(MERGE_IF_NECESSARY, r5.getChangeId());
+ assertSubmitType(MERGE_IF_NECESSARY, r6.getChangeId());
+
+ setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
+
+ assertSubmitType(MERGE_IF_NECESSARY, r1.getChangeId());
+ assertSubmitType(FAST_FORWARD_ONLY, r2.getChangeId());
+ assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId());
+ assertSubmitType(REBASE_IF_NECESSARY, r4.getChangeId());
+ assertSubmitType(MERGE_ALWAYS, r5.getChangeId());
+ assertSubmitType(CHERRY_PICK, r6.getChangeId());
+ }
+
+ @Test
+ public void submitTypeIsUsedForSubmit() throws Exception {
+ setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
+
+ PushOneCommit.Result r = createChange("master", "CHERRY_PICK 1");
+
+ gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(r.getChangeId()).current().submit();
+
+ List<RevCommit> log = log("master", 1);
+ assertThat(log.get(0).getShortMessage()).isEqualTo("CHERRY_PICK 1");
+ assertThat(log.get(0).name()).isNotEqualTo(r.getCommit().name());
+ assertThat(log.get(0).getFullMessage())
+ .contains("Change-Id: " + r.getChangeId());
+ assertThat(log.get(0).getFullMessage()).contains("Reviewed-on: ");
+ }
+
+ @Test
+ public void mixingSubmitTypesAcrossBranchesSucceeds() throws Exception {
+ setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
+
+ PushOneCommit.Result r1 = createChange("master", "MERGE_IF_NECESSARY 1");
+
+ RevCommit initialCommit = r1.getCommit().getParent(0);
+ BranchInput bin = new BranchInput();
+ bin.revision = initialCommit.name();
+ gApi.projects().name(project.get()).branch("branch").create(bin);
+
+ testRepo.reset(initialCommit);
+ PushOneCommit.Result r2 = createChange("branch", "MERGE_ALWAYS 1");
+
+ gApi.changes().id(r1.getChangeId()).topic(name("topic"));
+ gApi.changes().id(r1.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(r2.getChangeId()).topic(name("topic"));
+ gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(r2.getChangeId()).current().submit();
+
+ assertThat(log("master", 1).get(0).name()).isEqualTo(r1.getCommit().name());
+
+ List<RevCommit> branchLog = log("branch", 1);
+ assertThat(branchLog.get(0).getParents()).hasLength(2);
+ assertThat(branchLog.get(0).getParent(1).name())
+ .isEqualTo(r2.getCommit().name());
+ }
+
+ @Test
+ public void mixingSubmitTypesOnOneBranchFails() throws Exception {
+ setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);
+
+ PushOneCommit.Result r1 = createChange("master", "CHERRY_PICK 1");
+ PushOneCommit.Result r2 = createChange("master", "MERGE_IF_NECESSARY 2");
+
+ gApi.changes().id(r1.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.approve());
+
+ try {
+ gApi.changes().id(r2.getChangeId()).current().submit();
+ fail("Expected ResourceConflictException");
+ } catch (ResourceConflictException e) {
+ assertThat(e).hasMessage(
+ "Failed to submit 2 changes due to the following problems:\n"
+ + "Change " + r1.getChange().getId() + ": Change has submit type "
+ + "CHERRY_PICK, but previously chose submit type MERGE_IF_NECESSARY "
+ + "from change " + r2.getChange().getId() + " in the same batch");
+ }
+ }
+
+ private List<RevCommit> log(String commitish, int n) throws Exception {
+ try (Repository repo = repoManager.openRepository(project);
+ Git git = new Git(repo)) {
+ ObjectId id = repo.resolve(commitish);
+ assertThat(id).isNotNull();
+ return ImmutableList.copyOf(git.log().add(id).setMaxCount(n).call());
+ }
+ }
+
+ private void assertSubmitType(SubmitType expected, String id)
+ throws Exception {
+ assertThat(gApi.changes().id(id).current().submitType())
+ .isEqualTo(expected);
+ }
+}
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 73a02a5..d104a41 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
@@ -83,11 +83,8 @@
}
private void saveProjectConfig(ProjectConfig cfg) throws Exception {
- MetaDataUpdate md = metaDataUpdateFactory.create(project);
- try {
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
cfg.commit(md);
- } finally {
- md.close();
}
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
index 78ffa20..6e27099 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -26,7 +26,6 @@
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.git.CommitMergeStatus;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -117,7 +116,8 @@
push("refs/for/master%submit", "other change", "a.txt", "other content");
r.assertErrorStatus();
r.assertChange(Change.Status.NEW, null);
- r.assertMessage(CommitMergeStatus.PATH_CONFLICT.getMessage());
+ r.assertMessage("Change " + r.getChange().getId()
+ + ": change could not be merged due to a path conflict.");
}
@Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
index 2b3d035..72fcd03 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -77,8 +77,9 @@
PushOneCommit.Result change2 =
createChange("Change 2", "a.txt", "other content");
submitWithConflict(change2.getChangeId(),
- "Cannot merge " + change2.getCommit().name() + "\n" +
- "Change could not be merged due to a path conflict.\n\n" +
+ "Failed to submit 1 change due to the following problems:\n" +
+ "Change " + change2.getChange().getId() + ": " +
+ "Change could not be merged due to a path conflict. " +
"Please rebase the change locally " +
"and upload the rebased commit for review.");
assertThat(getRemoteHead()).isEqualTo(oldHead);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
index b04dc6d..875725f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -30,12 +30,9 @@
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.group.SystemGroupBackend;
-import org.eclipse.jgit.errors.ConfigInvalidException;
import org.junit.Before;
import org.junit.Test;
-import java.io.IOException;
-
public class ChangeOwnerIT extends AbstractDaemonTest {
private TestAccount user2;
@@ -80,20 +77,20 @@
approve(a, changeId);
}
- private void grantApproveToChangeOwner() throws IOException,
- ConfigInvalidException {
- MetaDataUpdate md = metaDataUpdateFactory.create(project);
- md.setMessage(String.format("Grant approve to change owner"));
- ProjectConfig config = ProjectConfig.read(md);
- AccessSection s = config.getAccessSection("refs/heads/*", true);
- Permission p = s.getPermission(LABEL + "Code-Review", true);
- PermissionRule rule = new PermissionRule(config
- .resolve(SystemGroupBackend.getGroup(SystemGroupBackend.CHANGE_OWNER)));
- rule.setMin(-2);
- rule.setMax(+2);
- p.add(rule);
- config.commit(md);
- projectCache.evict(config.getProject());
+ private void grantApproveToChangeOwner() throws Exception {
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
+ md.setMessage(String.format("Grant approve to change owner"));
+ ProjectConfig config = ProjectConfig.read(md);
+ AccessSection s = config.getAccessSection("refs/heads/*", true);
+ Permission p = s.getPermission(LABEL + "Code-Review", true);
+ PermissionRule rule = new PermissionRule(config
+ .resolve(SystemGroupBackend.getGroup(SystemGroupBackend.CHANGE_OWNER)));
+ rule.setMin(-2);
+ rule.setMax(+2);
+ p.add(rule);
+ config.commit(md);
+ projectCache.evict(config.getProject());
+ }
}
private String createMyChange() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
index efaa1df..65d2a23 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -16,7 +16,6 @@
import static com.google.common.truth.Truth.assertThat;
-import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.extensions.client.ChangeStatus;
@@ -106,9 +105,9 @@
PushOneCommit.Result change2 =
createChange("Change 2", "a.txt", "other content");
submitWithConflict(change2.getChangeId(),
- "Cannot merge " + change2.getCommitId().name() + "\n" +
- "Change could not be merged due to a path conflict.\n\n" +
- "Please rebase the change locally and " +
+ "Failed to submit 1 change due to the following problems:\n" +
+ "Change " + change2.getChange().getId() + ": Change could not be " +
+ "merged due to a path conflict. Please rebase the change locally and " +
"upload the rebased commit for review.");
assertThat(getRemoteHead()).isEqualTo(oldHead);
@@ -151,9 +150,9 @@
PushOneCommit.Result change3 =
createChange("Change 3", "b.txt", "different content");
submitWithConflict(change3.getChangeId(),
- "Cannot merge " + change3.getCommitId().name() + "\n" +
- "Change could not be merged due to a path conflict.\n\n" +
- "Please rebase the change locally and " +
+ "Failed to submit 1 change due to the following problems:\n" +
+ "Change " + change3.getChange().getId() + ": Change could not be " +
+ "merged due to a path conflict. Please rebase the change locally and " +
"upload the rebased commit for review.");
assertThat(getRemoteHead()).isEqualTo(oldHead);
@@ -227,15 +226,13 @@
// Submit fails; change3 contains the delta "b1" -> "b2", which cannot be
// applied against tip.
submitWithConflict(change3.getChangeId(),
- "Cannot merge " + change3.getCommitId().name() + "\n" +
- "Change could not be merged due to a path conflict.\n\n" +
- "Please rebase the change locally and " +
+ "Failed to submit 1 change due to the following problems:\n" +
+ "Change " + change3.getChange().getId() + ": Change could not be " +
+ "merged due to a path conflict. Please rebase the change locally and " +
"upload the rebased commit for review.");
ChangeInfo info3 = get(change3.getChangeId(), ListChangesOption.MESSAGES);
assertThat(info3.status).isEqualTo(ChangeStatus.NEW);
- assertThat(Iterables.getLast(info3.messages).message.toLowerCase())
- .contains("path conflict");
// Tip has not changed.
List<RevCommit> log = getRemoteLog();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
index a87b7d9..ead5419 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -73,9 +73,8 @@
Change.Id id1 = change1.getPatchSetId().getParentKey();
submitWithConflict(change2.getChangeId(),
- "The change could not be submitted because it depends on change(s) [" +
- id1 + "], which could not be submitted because:\n" +
- id1 + ": needs Code-Review;");
+ "Failed to submit 2 changes due to the following problems:\n"
+ + "Change " + id1 + ": needs Code-Review");
RevCommit head = getRemoteHead();
assertThat(head.getId()).isEqualTo(oldHead.getId());
@@ -101,9 +100,10 @@
assertThat(info.enabled).isNull();
submitWithConflict(change2.getChangeId(),
- "Cannot merge " + change2.getCommitId().name() + "\n" +
- "Project policy requires all submissions to be a fast-forward.\n\n" +
- "Please rebase the change locally and upload again for review.");
+ "Failed to submit 1 change due to the following problems:\n" +
+ "Change " + change2.getChange().getId() + ": Project policy requires " +
+ "all submissions to be a fast-forward. Please rebase the change " +
+ "locally and upload again for review.");
assertThat(getRemoteHead()).isEqualTo(oldHead);
assertSubmitter(change.getChangeId(), 1);
}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 1aa0e85..7fcf46f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -181,9 +181,9 @@
if (isSubmitWholeTopicEnabled()) {
submitWithConflict(change1b.getChangeId(),
- "Cannot merge " + change3.getCommit().name() + "\n" +
- "Change could not be merged due to a path conflict.\n\n" +
- "Please rebase the change locally " +
+ "Failed to submit 5 changes due to the following problems:\n" +
+ "Change " + change3.getChange().getId() + ": Change could not be " +
+ "merged due to a path conflict. Please rebase the change locally " +
"and upload the rebased commit for review.");
} else {
submit(change1b.getChangeId());
@@ -296,7 +296,11 @@
"a.txt", "1", "a-topic-here");
approve(change3b.getChangeId());
- submitWithConflict(change3a.getChangeId(), "Merge Conflict");
+ String cnt = isSubmitWholeTopicEnabled() ? "2 changes" : "1 change";
+ submitWithConflict(change3a.getChangeId(),
+ "Failed to submit " + cnt + " due to the following problems:\n"
+ + "Change " + change3a.getChange().getId() + ": depends on change that"
+ + " was not submitted");
RevCommit tipbranch = getRemoteLog(project, "branch").get(0);
assertThat(tipbranch.getShortMessage()).isEqualTo(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
index f7e11fb..534afcb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/LabelTypeIT.java
@@ -321,11 +321,8 @@
}
private void saveProjectConfig(ProjectConfig cfg) throws Exception {
- MetaDataUpdate md = metaDataUpdateFactory.create(project);
- try {
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
cfg.commit(md);
- } finally {
- md.close();
}
}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
index 5a79d08..cd226ef 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
@@ -32,15 +32,33 @@
}
public static SubmitTypeRecord OK(SubmitType type) {
- SubmitTypeRecord r = new SubmitTypeRecord();
- r.status = Status.OK;
- r.type = type;
- return r;
+ return new SubmitTypeRecord(Status.OK, type, null);
}
- public Status status;
- public SubmitType type;
- public String errorMessage;
+ public static SubmitTypeRecord error(String err) {
+ return new SubmitTypeRecord(SubmitTypeRecord.Status.RULE_ERROR, null, err);
+ }
+
+ /** Status enum value of the record. */
+ public final Status status;
+
+ /** Submit type of the record; never null if {@link #status} is {@code OK}. */
+ public final SubmitType type;
+
+ /**
+ * Submit type of the record; always null if {@link #status} is {@code OK}.
+ */
+ public final String errorMessage;
+
+ private SubmitTypeRecord(Status status, SubmitType type, String errorMessage) {
+ this.status = status;
+ this.type = type;
+ this.errorMessage = errorMessage;
+ }
+
+ public boolean isOk() {
+ return status == Status.OK;
+ }
@Override
public String toString() {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
index 32f8488..9cddda9 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -38,6 +38,11 @@
AccountApi id(String id) throws RestApiException;
/**
+ * @see #id(String)
+ */
+ AccountApi id(int id) throws RestApiException;
+
+ /**
* Look up the account of the current in-scope user.
*
* @see #id(String)
@@ -118,6 +123,11 @@
}
@Override
+ public AccountApi id(int id) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public AccountApi self() throws RestApiException {
throw new NotImplementedException();
}
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 d927231..870fef4 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
@@ -62,7 +62,7 @@
* Look up the reviewer of the change.
* <p>
* @param id ID of the account, can be a string of the format
- * "Full Name <mail@example.com>", just the email address, a full name
+ * "Full Name <mail@example.com>", just the email address, a full name
* if it is unique, an account ID, a user name or 'self' for the
* calling user.
* @return API for accessing the reviewer.
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 44c2ba4..6ff7889 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
@@ -14,10 +14,12 @@
package com.google.gerrit.extensions.api.changes;
+import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ActionInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.extensions.common.MergeableInfo;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -28,6 +30,7 @@
public interface RevisionApi {
void delete() throws RestApiException;
+
void review(ReviewInput in) throws RestApiException;
void submit() throws RestApiException;
@@ -65,6 +68,9 @@
Map<String, ActionInfo> actions() throws RestApiException;
+ SubmitType submitType() throws RestApiException;
+ SubmitType testSubmitType(TestSubmitRuleInput in) throws RestApiException;
+
/**
* A default implementation which allows source compatibility
* when adding new methods to the interface.
@@ -194,5 +200,16 @@
public Map<String, ActionInfo> actions() throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public SubmitType submitType() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public SubmitType testSubmitType(TestSubmitRuleInput in)
+ throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthLoginProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthLoginProvider.java
new file mode 100644
index 0000000..3fa7bb2
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/auth/oauth/OAuthLoginProvider.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.auth.oauth;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.io.IOException;
+
+@ExtensionPoint
+public interface OAuthLoginProvider {
+
+ /**
+ * Performs a login with an OAuth2 provider for Git over HTTP
+ * communication.
+ *
+ * An implementation of this interface must transmit the given
+ * user name and secret, which can be either an OAuth2 access token
+ * or a password, to the OAuth2 backend for verification.
+ *
+ * @param username the user's identifier.
+ * @param secret the secret to verify, e.g. a previously received
+ * access token or a password.
+ *
+ * @return information about the logged in user, at least
+ * external id, user name and email address.
+ *
+ * @throws IOException if the login failed.
+ */
+ OAuthUserInfo login(String username, String secret) throws IOException;
+}
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 455243a..cc7fb77 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
@@ -16,6 +16,7 @@
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.ReviewerState;
+import com.google.gerrit.extensions.client.SubmitType;
import java.sql.Timestamp;
import java.util.Collection;
@@ -35,6 +36,7 @@
public Timestamp updated;
public Boolean starred;
public Boolean reviewed;
+ public SubmitType submitType;
public Boolean mergeable;
public Boolean submittable;
public Integer insertions;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TestSubmitRuleInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TestSubmitRuleInput.java
new file mode 100644
index 0000000..96a1626
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/TestSubmitRuleInput.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+public class TestSubmitRuleInput {
+ public enum Filters {
+ RUN, SKIP
+ }
+
+ @DefaultInput
+ public String rule;
+ public Filters filters;
+}
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 a136007..a0bd4bf 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
@@ -40,6 +40,7 @@
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResource;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -70,16 +71,19 @@
private final DynamicMap<RestView<GpgKey>> views;
private final Provider<ReviewDb> db;
+ private final Provider<CurrentUser> self;
private final Provider<PublicKeyStore> storeProvider;
private final GerritPublicKeyChecker.Factory checkerFactory;
@Inject
GpgKeys(DynamicMap<RestView<GpgKey>> views,
Provider<ReviewDb> db,
+ Provider<CurrentUser> self,
Provider<PublicKeyStore> storeProvider,
GerritPublicKeyChecker.Factory checkerFactory) {
this.views = views;
this.db = db;
+ this.self = self;
this.storeProvider = storeProvider;
this.checkerFactory = checkerFactory;
}
@@ -87,7 +91,6 @@
@Override
public ListGpgKeys list()
throws ResourceNotFoundException, AuthException {
- checkEnabled();
return new ListGpgKeys();
}
@@ -95,7 +98,7 @@
public GpgKey parse(AccountResource parent, IdString id)
throws ResourceNotFoundException, PGPException, OrmException,
IOException {
- checkEnabled();
+ checkVisible(self, parent);
String str = CharMatcher.WHITESPACE.removeFrom(id.get()).toUpperCase();
if ((str.length() != 8 && str.length() != 40)
|| !CharMatcher.anyOf("0123456789ABCDEF").matchesAllOf(str)) {
@@ -151,7 +154,9 @@
public class ListGpgKeys implements RestReadView<AccountResource> {
@Override
public Map<String, GpgKeyInfo> apply(AccountResource rsrc)
- throws OrmException, PGPException, IOException {
+ throws OrmException, PGPException, IOException,
+ ResourceNotFoundException {
+ checkVisible(self, rsrc);
Map<String, GpgKeyInfo> keys = new HashMap<>();
try (PublicKeyStore store = storeProvider.get()) {
for (AccountExternalId extId : getGpgExtIds(rsrc)) {
@@ -225,10 +230,14 @@
return NB.decodeInt64(fp, fp.length - 8);
}
- static void checkEnabled() throws ResourceNotFoundException {
+ static void checkVisible(Provider<CurrentUser> self, AccountResource rsrc)
+ throws ResourceNotFoundException {
if (!BouncyCastleUtil.havePGP()) {
throw new ResourceNotFoundException("GPG not enabled");
}
+ if (self.get() != rsrc.getUser()) {
+ throw new ResourceNotFoundException();
+ }
}
public static GpgKeyInfo toJson(PGPPublicKey key, CheckResult checkResult)
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 91c4494..20ef0db7 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
@@ -41,6 +41,7 @@
import com.google.gerrit.gpg.server.PostGpgKeys.Input;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource;
@@ -80,6 +81,7 @@
private final Logger log = LoggerFactory.getLogger(getClass());
private final Provider<PersonIdent> serverIdent;
private final Provider<ReviewDb> db;
+ private final Provider<CurrentUser> self;
private final Provider<PublicKeyStore> storeProvider;
private final GerritPublicKeyChecker.Factory checkerFactory;
private final AddKeySender.Factory addKeyFactory;
@@ -87,11 +89,13 @@
@Inject
PostGpgKeys(@GerritPersonIdent Provider<PersonIdent> serverIdent,
Provider<ReviewDb> db,
+ Provider<CurrentUser> self,
Provider<PublicKeyStore> storeProvider,
GerritPublicKeyChecker.Factory checkerFactory,
AddKeySender.Factory addKeyFactory) {
this.serverIdent = serverIdent;
this.db = db;
+ this.self = self;
this.storeProvider = storeProvider;
this.checkerFactory = checkerFactory;
this.addKeyFactory = addKeyFactory;
@@ -101,7 +105,7 @@
public Map<String, GpgKeyInfo> apply(AccountResource rsrc, Input input)
throws ResourceNotFoundException, BadRequestException,
ResourceConflictException, PGPException, OrmException, IOException {
- GpgKeys.checkEnabled();
+ GpgKeys.checkVisible(self, rsrc);
List<AccountExternalId> existingExtIds =
GpgKeys.getGpgExtIds(db.get(), rsrc.getUser().getAccountId()).toList();
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 c5639c1..9d0676e 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
@@ -19,6 +19,7 @@
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.common.data.LabelValue;
import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
@@ -139,6 +140,15 @@
public final native boolean _more_changes()
/*-{ return this._more_changes ? true : false; }-*/;
+ public final SubmitType submitType() {
+ String submitType = _submitType();
+ if (submitType == null) {
+ return null;
+ }
+ return SubmitType.valueOf(submitType);
+ }
+ private final native String _submitType() /*-{ return this.submit_type; }-*/;
+
public final boolean submittable() {
init();
return _submittable();
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 60d21ba..b0a0c68 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
@@ -45,7 +45,6 @@
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
@@ -1055,33 +1054,16 @@
}));
}
- private void loadSubmitType(final Change.Status status, final boolean canSubmit) {
- if (canSubmit) {
- if (status == Change.Status.NEW) {
- statusText.setInnerText(Util.C.readyToSubmit());
- }
+ private void renderSubmitType(Change.Status status, boolean canSubmit,
+ SubmitType submitType) {
+ if (canSubmit && status == Change.Status.NEW) {
+ statusText.setInnerText(changeInfo.mergeable()
+ ? Util.C.readyToSubmit()
+ : Util.C.mergeConflict());
}
- ChangeApi.revision(changeId.get(), revision)
- .view("submit_type")
- .get(new AsyncCallback<NativeString>() {
- @Override
- public void onSuccess(NativeString result) {
- if (canSubmit) {
- if (status == Change.Status.NEW) {
- statusText.setInnerText(changeInfo.mergeable()
- ? Util.C.readyToSubmit()
- : Util.C.mergeConflict());
- }
- }
- setVisible(notMergeable, !changeInfo.mergeable());
-
- renderSubmitType(result.asString());
- }
-
- @Override
- public void onFailure(Throwable caught) {
- }
- });
+ setVisible(notMergeable, !changeInfo.mergeable());
+ submitActionText.setInnerText(
+ com.google.gerrit.client.admin.Util.toLongString(submitType));
}
private RevisionInfo resolveRevisionToDisplay(ChangeInfo info) {
@@ -1243,7 +1225,7 @@
if (current && info.status().isOpen()) {
quickApprove.set(info, revision, replyAction);
- loadSubmitType(info.status(), isSubmittable(info));
+ renderSubmitType(info.status(), isSubmittable(info), info.submitType());
} else {
quickApprove.setVisible(false);
}
@@ -1348,16 +1330,6 @@
return sb.toString();
}
- private void renderSubmitType(String action) {
- try {
- SubmitType type = SubmitType.valueOf(action);
- submitActionText.setInnerText(
- com.google.gerrit.client.admin.Util.toLongString(type));
- } catch (IllegalArgumentException e) {
- submitActionText.setInnerText(action);
- }
- }
-
private void renderActionTextDate(ChangeInfo info) {
String action;
if (info.created().equals(info.updated())) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java
index a0480d0..db78f39 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java
@@ -182,7 +182,7 @@
PatchSet.Id psId =
new PatchSet.Id(changeId, Integer.parseInt(elements[offset + 1]));
path = atob(elements[offset + 2]);
- side = (Side.PARENT.toString() == elements[offset + 3]) ? Side.PARENT
+ side = (Side.PARENT.toString().equals(elements[offset + 3])) ? Side.PARENT
: Side.REVISION;
range = null;
if (elements[offset + 4].startsWith("R")) {
diff --git a/gerrit-httpd/BUCK b/gerrit-httpd/BUCK
index 3085054..f4254dc 100644
--- a/gerrit-httpd/BUCK
+++ b/gerrit-httpd/BUCK
@@ -36,7 +36,7 @@
'//lib/jgit:jgit',
'//lib/jgit:jgit-servlet',
'//lib/log:api',
- '//lib/lucene:core-and-backward-codecs',
+ '//lib/lucene:lucene-core',
],
provided_deps = ['//lib:servlet-api-3_1'],
visibility = ['PUBLIC'],
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
index e1810ef..ab6cc90 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -14,6 +14,8 @@
package com.google.gerrit.httpd;
+import static com.google.gerrit.reviewdb.client.AuthType.OAUTH;
+
import com.google.gerrit.reviewdb.client.CoreDownloadSchemes;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.DownloadConfig;
@@ -40,7 +42,11 @@
if (authConfig.isTrustContainerAuth()) {
authFilter = ContainerAuthFilter.class;
} else if (authConfig.isGitBasicAuth()) {
- authFilter = ProjectBasicAuthFilter.class;
+ if (authConfig.getAuthType() == OAUTH) {
+ authFilter = ProjectOAuthFilter.class;
+ } else {
+ authFilter = ProjectBasicAuthFilter.class;
+ }
} else {
authFilter = ProjectDigestFilter.class;
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectOAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
new file mode 100644
index 0000000..75f3b2c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectOAuthFilter.java
@@ -0,0 +1,336 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicMap.Entry;
+import com.google.gerrit.server.AccessPath;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+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.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.apache.commons.codec.binary.Base64;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Locale;
+import java.util.NoSuchElementException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+/**
+ * Authenticates the current user with an OAuth2 server.
+ *
+ * @see <a href="https://tools.ietf.org/rfc/rfc6750.txt">RFC 6750</a>
+ */
+@Singleton
+class ProjectOAuthFilter implements Filter {
+
+ private static final Logger log = LoggerFactory
+ .getLogger(ProjectOAuthFilter.class);
+
+ private static final String REALM_NAME = "Gerrit Code Review";
+ private static final String AUTHORIZATION = "Authorization";
+ private static final String BASIC = "Basic ";
+ private static final String GIT_COOKIE_PREFIX = "git-";
+
+ private final DynamicItem<WebSession> session;
+ private final DynamicMap<OAuthLoginProvider> loginProviders;
+ private final AccountCache accountCache;
+ private final AccountManager accountManager;
+ private final String gitOAuthProvider;
+ private final boolean userNameToLowerCase;
+
+ private String defaultAuthPlugin;
+ private String defaultAuthProvider;
+
+ @Inject
+ ProjectOAuthFilter(DynamicItem<WebSession> session,
+ DynamicMap<OAuthLoginProvider> pluginsProvider,
+ AccountCache accountCache,
+ AccountManager accountManager,
+ @GerritServerConfig Config gerritConfig) {
+ this.session = session;
+ this.loginProviders = pluginsProvider;
+ this.accountCache = accountCache;
+ this.accountManager = accountManager;
+ this.gitOAuthProvider =
+ gerritConfig.getString("auth", null, "gitOAuthProvider");
+ this.userNameToLowerCase =
+ gerritConfig.getBoolean("auth", null, "userNameToLowerCase", false);
+ }
+
+ @Override
+ public void init(FilterConfig config) throws ServletException {
+ if (Strings.isNullOrEmpty(gitOAuthProvider)) {
+ pickOnlyProvider();
+ } else {
+ pickConfiguredProvider();
+ }
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest req = (HttpServletRequest) request;
+ Response rsp = new Response((HttpServletResponse) response);
+ if (verify(req, rsp)) {
+ chain.doFilter(req, rsp);
+ }
+ }
+
+ private boolean verify(HttpServletRequest req, Response rsp)
+ throws IOException {
+ AuthInfo authInfo = null;
+
+ // first check if there is a suitable git cookie; such cookies are
+ // expected to have names starting with the prefix git-
+ if (req.getCookies() != null) {
+ for (Cookie cookie: req.getCookies()) {
+ if (cookie.getName().startsWith(GIT_COOKIE_PREFIX)) {
+ authInfo = extractAuthInfo(cookie);
+ if (authInfo != null) {
+ break;
+ }
+ }
+ }
+ }
+
+ // if there is no suitable git cookie fall back to Basic authentication
+ if (authInfo == null) {
+ String hdr = req.getHeader(AUTHORIZATION);
+ if (hdr == null || !hdr.startsWith(BASIC)) {
+ // Allow an anonymous connection through, or it might be using a
+ // session cookie instead of basic authentication.
+ return true;
+ }
+
+ byte[] decoded = Base64.decodeBase64(hdr.substring(BASIC.length()));
+ String usernamePassword = new String(decoded, encoding(req));
+ int splitPos = usernamePassword.indexOf(':');
+ if (splitPos < 1) {
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+
+ authInfo = new AuthInfo(usernamePassword.substring(0, splitPos),
+ usernamePassword.substring(splitPos + 1),
+ defaultAuthPlugin, defaultAuthProvider);
+ }
+
+ if (Strings.isNullOrEmpty(authInfo.tokenOrSecret)) {
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+
+ AccountState who = accountCache.getByUsername(authInfo.username);
+ if (who == null || !who.getAccount().isActive()) {
+ log.warn("Authentication failed for " + authInfo.username
+ + ": account inactive or not provisioned in Gerrit");
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+
+ AuthRequest authRequest = AuthRequest.forExternalUser(
+ authInfo.username);
+ authRequest.setEmailAddress(who.getAccount().getPreferredEmail());
+ authRequest.setDisplayName(who.getAccount().getFullName());
+ authRequest.setPassword(authInfo.tokenOrSecret);
+ authRequest.setAuthPlugin(authInfo.pluginName);
+ authRequest.setAuthProvider(authInfo.exportName);
+
+ try {
+ AuthResult authResult = accountManager.authenticate(authRequest);
+ WebSession ws = session.get();
+ ws.setUserAccountId(authResult.getAccountId());
+ ws.setAccessPathOk(AccessPath.GIT, true);
+ ws.setAccessPathOk(AccessPath.REST_API, true);
+ return true;
+ } catch (AccountException e) {
+ log.warn("Authentication failed for " + authInfo.username, e);
+ rsp.sendError(SC_UNAUTHORIZED);
+ return false;
+ }
+ }
+
+ /**
+ * Picks the only installed OAuth provider. If there is a multiude
+ * of providers available, the actual provider must be determined
+ * from the authentication request.
+ *
+ * @throws ServletException if there is no {@code OAuthLoginProvider}
+ * installed at all.
+ */
+ private void pickOnlyProvider() throws ServletException {
+ try {
+ Entry<OAuthLoginProvider> loginProvider =
+ Iterables.getOnlyElement(loginProviders);
+ defaultAuthPlugin = loginProvider.getPluginName();
+ defaultAuthProvider = loginProvider.getExportName();
+ } catch (NoSuchElementException e) {
+ throw new ServletException("No OAuth login provider installed");
+ } catch (IllegalArgumentException e) {
+ // multiple providers found => do not pick any
+ }
+ }
+
+ /**
+ * Picks the {@code OAuthLoginProvider} configured with
+ * <tt>auth.gitOAuthProvider</tt>.
+ *
+ * @throws ServletException if the configured provider was not found.
+ */
+ private void pickConfiguredProvider() throws ServletException {
+ int splitPos = gitOAuthProvider.lastIndexOf(':');
+ if (splitPos < 1 || splitPos == gitOAuthProvider.length() - 1) {
+ // no colon at all or leading/trailing colon: malformed providerId
+ throw new ServletException("OAuth login provider configuration is"
+ + " invalid: Must be of the form pluginName:providerName");
+ }
+ defaultAuthPlugin= gitOAuthProvider.substring(0, splitPos);
+ defaultAuthProvider = gitOAuthProvider.substring(splitPos + 1);
+ OAuthLoginProvider provider = loginProviders.get(defaultAuthPlugin,
+ defaultAuthProvider);
+ if (provider == null) {
+ throw new ServletException("Configured OAuth login provider "
+ + gitOAuthProvider + " wasn't installed");
+ }
+ }
+
+ private AuthInfo extractAuthInfo(Cookie cookie)
+ throws UnsupportedEncodingException {
+ String username = URLDecoder.decode(cookie.getName()
+ .substring(GIT_COOKIE_PREFIX.length()), UTF_8.name());
+ String value = cookie.getValue();
+ int splitPos = value.lastIndexOf('@');
+ if (splitPos < 1 || splitPos == value.length() - 1) {
+ // no providerId in the cookie value => assume default provider
+ // note: a leading/trailing at sign is considered to belong to
+ // the access token rather than being a separator
+ return new AuthInfo(username, cookie.getValue(),
+ defaultAuthPlugin, defaultAuthProvider);
+ }
+ String token = value.substring(0, splitPos);
+ String providerId = value.substring(splitPos + 1);
+ splitPos = providerId.lastIndexOf(':');
+ if (splitPos < 1 || splitPos == providerId.length() - 1) {
+ // no colon at all or leading/trailing colon: malformed providerId
+ return null;
+ }
+ String pluginName = providerId.substring(0, splitPos);
+ String exportName = providerId.substring(splitPos + 1);
+ OAuthLoginProvider provider = loginProviders.get(pluginName, exportName);
+ if (provider == null) {
+ return null;
+ }
+ return new AuthInfo(username, token, pluginName, exportName);
+ }
+
+ private static String encoding(HttpServletRequest req) {
+ return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
+ }
+
+ private class AuthInfo {
+ private final String username;
+ private final String tokenOrSecret;
+ private final String pluginName;
+ private final String exportName;
+
+ private AuthInfo(String username, String tokenOrSecret,
+ String pluginName, String exportName) {
+ this.username = userNameToLowerCase
+ ? username.toLowerCase(Locale.US)
+ : username;
+ this.tokenOrSecret = tokenOrSecret;
+ this.pluginName = pluginName;
+ this.exportName = exportName;
+ }
+ }
+
+ private static class Response extends HttpServletResponseWrapper {
+ private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
+
+ Response(HttpServletResponse rsp) {
+ super(rsp);
+ }
+
+ private void status(int sc) {
+ if (sc == SC_UNAUTHORIZED) {
+ StringBuilder v = new StringBuilder();
+ v.append(BASIC);
+ v.append("realm=\"").append(REALM_NAME).append("\"");
+ setHeader(WWW_AUTHENTICATE, v.toString());
+ } else if (containsHeader(WWW_AUTHENTICATE)) {
+ setHeader(WWW_AUTHENTICATE, null);
+ }
+ }
+
+ @Override
+ public void sendError(int sc, String msg) throws IOException {
+ status(sc);
+ super.sendError(sc, msg);
+ }
+
+ @Override
+ public void sendError(int sc) throws IOException {
+ status(sc);
+ super.sendError(sc);
+ }
+
+ @Override
+ @Deprecated
+ public void setStatus(int sc, String sm) {
+ status(sc);
+ super.setStatus(sc, sm);
+ }
+
+ @Override
+ public void setStatus(int sc) {
+ status(sc);
+ super.setStatus(sc);
+ }
+ }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index d8935fd..f3874be 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -21,7 +21,6 @@
import com.google.gerrit.httpd.raw.CatServlet;
import com.google.gerrit.httpd.raw.HostPageServlet;
import com.google.gerrit.httpd.raw.LegacyGerritServlet;
-import com.google.gerrit.httpd.raw.RobotsServlet;
import com.google.gerrit.httpd.raw.SshInfoServlet;
import com.google.gerrit.httpd.raw.ToolServlet;
import com.google.gerrit.httpd.rpc.access.AccessRestApiServlet;
@@ -104,8 +103,6 @@
serveRegex("^/(?:a/)?projects/(.*)?$").with(ProjectsRestApiServlet.class);
filter("/Documentation/").through(QueryDocumentationFilter.class);
-
- serve("/robots.txt").with(RobotsServlet.class);
}
private Key<HttpServlet> notFound() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
index ea345ef..d9aab24 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -75,6 +75,7 @@
.put("gif", "image/gif")
.put("htm", "text/html")
.put("html", "text/html")
+ .put("ico", "image/x-icon")
.put("jpeg", "image/jpeg")
.put("jpg", "image/jpeg")
.put("js", JS)
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java
deleted file mode 100644
index 2f5bc3a..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RobotsServlet.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (C) 2013 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.httpd.raw;
-
-import static java.nio.file.Files.exists;
-import static java.nio.file.Files.isReadable;
-
-import com.google.common.io.ByteStreams;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * This class provides a mechanism to use a configurable robots.txt file,
- * outside of the .war of the application. In order to configure it add the
- * following to the {@code httpd} section of the {@code gerrit.conf}
- * file:
- *
- * <pre>
- * [httpd]
- * robotsFile = etc/myrobots.txt
- * </pre>
- *
- * If the specified file name is relative it will resolved as a sub directory of
- * the site directory, if it is absolute it will be used as is.
- *
- * If the specified file doesn't exist or isn't readable the servlet will
- * default to the {@code robots.txt} file bundled with the .war file of the
- * application.
- */
-@SuppressWarnings("serial")
-@Singleton
-public class RobotsServlet extends HttpServlet {
- private static final Logger log =
- LoggerFactory.getLogger(RobotsServlet.class);
-
- private final Path robotsFile;
-
- @Inject
- RobotsServlet(@GerritServerConfig final Config config, final SitePaths sitePaths) {
- Path file = sitePaths.resolve(
- config.getString("httpd", null, "robotsFile"));
- if (file != null && (!exists(file) || !isReadable(file))) {
- log.warn("Cannot read httpd.robotsFile, using default");
- file = null;
- }
- robotsFile = file;
- }
-
- @Override
- protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp)
- throws IOException {
- rsp.setContentType("text/plain");
- try (InputStream in = openRobotsFile();
- OutputStream out = rsp.getOutputStream()) {
- ByteStreams.copy(in, out);
- }
- }
-
- private InputStream openRobotsFile() {
- if (robotsFile != null) {
- try {
- return Files.newInputStream(robotsFile);
- } catch (IOException e) {
- log.warn("Cannot read " + robotsFile + "; using default", e);
- }
- }
- return getServletContext().getResourceAsStream("/robots.txt");
- }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiIndexServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java
similarity index 75%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiIndexServlet.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java
index 3b225c9..d75a523 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/PolyGerritUiIndexServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SingleFileServlet.java
@@ -18,18 +18,19 @@
import java.nio.file.Path;
-class PolyGerritUiIndexServlet extends ResourceServlet {
+/** Serve a single static file, regardless of path. */
+class SingleFileServlet extends ResourceServlet{
private static final long serialVersionUID = 1L;
- private final Path index;
+ private final Path path;
- PolyGerritUiIndexServlet(Cache<Path, Resource> cache, Path ui) {
- super(cache, true);
- index = ui.resolve("index.html");
+ SingleFileServlet(Cache<Path, Resource> cache, Path path, boolean refresh) {
+ super(cache, refresh);
+ this.path = path;
}
@Override
protected Path getResourcePath(String pathInfo) {
- return index;
+ return path;
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
index da66ba3..a34137e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -15,6 +15,8 @@
package com.google.gerrit.httpd.raw;
import static com.google.common.base.Preconditions.checkArgument;
+import static java.nio.file.Files.exists;
+import static java.nio.file.Files.isReadable;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableList;
@@ -23,6 +25,8 @@
import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
import com.google.gerrit.launcher.GerritLauncher;
import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
import com.google.inject.Inject;
import com.google.inject.Key;
import com.google.inject.Provides;
@@ -32,6 +36,10 @@
import com.google.inject.name.Names;
import com.google.inject.servlet.ServletModule;
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -43,6 +51,9 @@
import javax.servlet.http.HttpServletResponse;
public class StaticModule extends ServletModule {
+ private static final Logger log =
+ LoggerFactory.getLogger(StaticModule.class);
+
public static final String CACHE = "static_content";
public static final ImmutableList<String> POLYGERRIT_INDEX_PATHS =
@@ -59,8 +70,12 @@
"/groups/*",
"/projects/*");
- private static final String GWT_UI_SERVLET = "GwtUiServlet";
private static final String DOC_SERVLET = "DocServlet";
+ private static final String FAVICON_SERVLET = "FaviconServlet";
+ private static final String GWT_UI_SERVLET = "GwtUiServlet";
+ private static final String POLYGERRIT_INDEX_SERVLET =
+ "PolyGerritUiIndexServlet";
+ private static final String ROBOTS_TXT_SERVLET = "RobotsTxtServlet";
private final GerritOptions options;
private Paths paths;
@@ -79,8 +94,7 @@
@Override
protected void configureServlets() {
- serveRegex("^/Documentation/(.+)$").with(
- Key.get(HttpServlet.class, Names.named(DOC_SERVLET)));
+ serveRegex("^/Documentation/(.+)$").with(named(DOC_SERVLET));
serve("/static/*").with(SiteStaticDirectoryServlet.class);
install(new CacheModule() {
@Override
@@ -91,8 +105,10 @@
}
});
if (options.enablePolyGerrit()) {
+ install(new CoreStaticModule());
install(new PolyGerritUiModule());
} else if (options.enableDefaultUi()) {
+ install(new CoreStaticModule());
install(new GwtUiModule());
}
}
@@ -117,6 +133,57 @@
}
}
+ private class CoreStaticModule extends ServletModule {
+ @Override
+ public void configureServlets() {
+ serve("/robots.txt").with(named(ROBOTS_TXT_SERVLET));
+ serve("/favicon.ico").with(named(FAVICON_SERVLET));
+ }
+
+ @Provides
+ @Singleton
+ @Named(ROBOTS_TXT_SERVLET)
+ HttpServlet getRobotsTxtServlet(@GerritServerConfig Config cfg,
+ SitePaths sitePaths, @Named(CACHE) Cache<Path, Resource> cache) {
+ Path configPath = sitePaths.resolve(
+ cfg.getString("httpd", null, "robotsFile"));
+ if (configPath != null) {
+ if (exists(configPath) && isReadable(configPath)) {
+ return new SingleFileServlet(cache, configPath, true);
+ } else {
+ log.warn("Cannot read httpd.robotsFile, using default");
+ }
+ }
+ Paths p = getPaths();
+ if (p.warFs != null) {
+ return new SingleFileServlet(
+ cache, p.warFs.getPath("/robots.txt"), false);
+ } else {
+ return new SingleFileServlet(
+ cache, webappSourcePath("robots.txt"), true);
+ }
+ }
+
+ @Provides
+ @Singleton
+ @Named(FAVICON_SERVLET)
+ HttpServlet getFaviconServlet(@Named(CACHE) Cache<Path, Resource> cache) {
+ Paths p = getPaths();
+ if (p.warFs != null) {
+ return new SingleFileServlet(
+ cache, p.warFs.getPath("/favicon.ico"), false);
+ } else {
+ return new SingleFileServlet(
+ cache, webappSourcePath("favicon.ico"), true);
+ }
+ }
+
+ private Path webappSourcePath(String name) {
+ return getPaths().buckOut.resolveSibling("gerrit-war").resolve("src")
+ .resolve("main").resolve("webapp").resolve(name);
+ }
+ }
+
private class GwtUiModule extends ServletModule {
@Override
public void configureServlets() {
@@ -163,18 +230,21 @@
// separate servlet.
}
+ Key<HttpServlet> indexKey = named(POLYGERRIT_INDEX_SERVLET);
for (String p : POLYGERRIT_INDEX_PATHS) {
filter(p).through(XsrfCookieFilter.class);
- serve(p).with(PolyGerritUiIndexServlet.class);
+ serve(p).with(indexKey);
}
serve("/*").with(PolyGerritUiServlet.class);
}
@Provides
@Singleton
- PolyGerritUiIndexServlet getPolyGerritUiIndexServlet(
+ @Named(POLYGERRIT_INDEX_SERVLET)
+ HttpServlet getPolyGerritUiIndexServlet(
@Named(CACHE) Cache<Path, Resource> cache) {
- return new PolyGerritUiIndexServlet(cache, polyGerritBasePath());
+ return new SingleFileServlet(
+ cache, polyGerritBasePath().resolve("index.html"), isDev());
}
@Provides
@@ -191,14 +261,17 @@
return new BowerComponentsServlet(cache, getPaths().buckOut);
}
+ private boolean isDev() {
+ return options.forcePolyGerritDev() || getPaths().warFs == null;
+ }
+
private Path polyGerritBasePath() {
Paths p = getPaths();
- boolean forceDev = options.forcePolyGerritDev();
- if (forceDev) {
+ if (options.forcePolyGerritDev()) {
checkArgument(p.buckOut != null,
"no buck-out directory found for PolyGerrit developer mode");
}
- return forceDev || p.warFs == null
+ return isDev()
? p.buckOut.getParent().resolve("polygerrit-ui").resolve("app")
: p.warFs.getPath("/polygerrit_ui");
}
@@ -280,4 +353,8 @@
}
}
}
+
+ private static Key<HttpServlet> named(String name) {
+ return Key.get(HttpServlet.class, Names.named(name));
+ }
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index aee238d..ed2a4f9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -100,8 +100,7 @@
// state, force a cache flush now.
//
ProjectConfig config;
- MetaDataUpdate md = metaDataUpdateFactory.create(projectName);
- try {
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
config = ProjectConfig.read(md);
if (config.updateGroupNames(groupBackend)) {
@@ -115,8 +114,6 @@
projectCache.evict(config.getProject());
pc = open();
}
- } finally {
- md.close();
}
final RefControl metaConfigControl = pc.controlForRef(RefNames.REFS_CONFIG);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
index ba4f012..f8d3e70 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -92,13 +92,7 @@
final ProjectControl projectControl =
projectControlFactory.controlFor(projectName);
- final MetaDataUpdate md;
- try {
- md = metaDataUpdateFactory.create(projectName);
- } catch (RepositoryNotFoundException notFound) {
- throw new NoSuchProjectException(projectName);
- }
- try {
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
ProjectConfig config = ProjectConfig.read(md, base);
Set<String> toDelete = scanSectionNames(config);
@@ -163,8 +157,8 @@
return updateProjectConfig(projectControl, config, md,
parentProjectUpdate);
- } finally {
- md.close();
+ } catch (RepositoryNotFoundException notFound) {
+ throw new NoSuchProjectException(projectName);
}
}
diff --git a/gerrit-lucene/BUCK b/gerrit-lucene/BUCK
index 8ba7479..e9490d0 100644
--- a/gerrit-lucene/BUCK
+++ b/gerrit-lucene/BUCK
@@ -11,7 +11,7 @@
'//gerrit-server:server',
'//lib:gwtorm',
'//lib:guava',
- '//lib/lucene:core-and-backward-codecs',
+ '//lib/lucene:lucene-core',
],
visibility = ['PUBLIC'],
)
@@ -33,9 +33,9 @@
'//lib/guice:guice-assistedinject',
'//lib/jgit:jgit',
'//lib/log:api',
- '//lib/lucene:analyzers-common',
- '//lib/lucene:core-and-backward-codecs',
- '//lib/lucene:misc',
+ '//lib/lucene:lucene-analyzers-common',
+ '//lib/lucene:lucene-core',
+ '//lib/lucene:lucene-misc',
],
visibility = ['PUBLIC'],
)
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 c05c8f0..de860f1 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
@@ -15,9 +15,9 @@
package com.google.gerrit.lucene;
import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Iterables.getOnlyElement;
import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
+import static com.google.gerrit.server.index.ChangeField.LEGACY_ID;
import static com.google.gerrit.server.index.IndexRewriter.CLOSED_STATUSES;
import static com.google.gerrit.server.index.IndexRewriter.OPEN_STATUSES;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -54,7 +54,6 @@
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeDataSource;
-import com.google.gerrit.server.query.change.LegacyChangeIdPredicate;
import com.google.gerrit.server.query.change.QueryOptions;
import com.google.gwtorm.protobuf.ProtobufCodec;
import com.google.gwtorm.server.OrmException;
@@ -74,8 +73,6 @@
import org.apache.lucene.document.StoredField;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
-import org.apache.lucene.index.DirectoryReader;
-import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
@@ -92,7 +89,6 @@
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.TopFieldDocs;
import org.apache.lucene.store.RAMDirectory;
-import org.apache.lucene.uninverting.UninvertingReader;
import org.apache.lucene.util.BytesRef;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@@ -137,6 +133,8 @@
ChangeField.REVIEWEDBY.getName();
private static final String UPDATED_SORT_FIELD =
sortFieldName(ChangeField.UPDATED);
+ private static final String ID_SORT_FIELD =
+ sortFieldName(ChangeField.LEGACY_ID);
private static final Map<String, String> CUSTOM_CHAR_MAPPING = ImmutableMap.of(
"_", " ", ".", " ");
@@ -206,18 +204,6 @@
private final QueryBuilder queryBuilder;
private final SubIndex openIndex;
private final SubIndex closedIndex;
- private final String idSortField;
-
- /**
- * Whether to use DocValues for range/sorted numeric fields.
- * <p>
- * Lucene 5 removed support for sorting based on normal numeric fields, so we
- * use the newer API for more strongly typed numeric fields in newer schema
- * versions. These fields also are not stored, so we need to store auxiliary
- * stored-only field for them as well.
- */
- // TODO(dborowitz): Delete when we delete support for pre-Lucene-5.0 schemas.
- private final boolean useDocValuesForSorting;
@AssistedInject
LuceneChangeIndex(
@@ -235,8 +221,6 @@
this.db = db;
this.changeDataFactory = changeDataFactory;
this.schema = schema;
- this.useDocValuesForSorting = schema.getVersion() >= 15;
- this.idSortField = sortFieldName(LegacyChangeIdPredicate.idField(schema));
CustomMappingAnalyzer analyzer =
new CustomMappingAnalyzer(new StandardAnalyzer(CharArraySet.EMPTY_SET),
@@ -251,7 +235,7 @@
GerritIndexWriterConfig closedConfig =
new GerritIndexWriterConfig(cfg, "changes_closed");
- SearcherFactory searcherFactory = newSearcherFactory();
+ SearcherFactory searcherFactory = new SearcherFactory();
if (cfg.getBoolean("index", "lucene", "testInmemory", false)) {
openIndex = new SubIndex(new RAMDirectory(), "ramOpen", openConfig,
searcherFactory);
@@ -267,26 +251,6 @@
}
}
- private SearcherFactory newSearcherFactory() {
- if (useDocValuesForSorting) {
- return new SearcherFactory();
- }
- @SuppressWarnings("deprecation")
- final Map<String, UninvertingReader.Type> mapping = ImmutableMap.of(
- ChangeField.LEGACY_ID.getName(), UninvertingReader.Type.INTEGER,
- ChangeField.UPDATED.getName(), UninvertingReader.Type.LONG);
- return new SearcherFactory() {
- @Override
- public IndexSearcher newSearcher(IndexReader reader, IndexReader previousReader)
- throws IOException {
- checkState(reader instanceof DirectoryReader,
- "expected DirectoryReader, found %s", reader.getClass().getName());
- return new IndexSearcher(
- UninvertingReader.wrap((DirectoryReader) reader, mapping));
- }
- };
- }
-
@Override
public void close() {
List<ListenableFuture<?>> closeFutures = Lists.newArrayListWithCapacity(2);
@@ -312,7 +276,7 @@
@Override
public void replace(ChangeData cd) throws IOException {
- Term id = QueryBuilder.idTerm(schema, cd);
+ Term id = QueryBuilder.idTerm(cd);
Document doc = toDocument(cd);
try {
if (cd.change().getStatus().isOpen()) {
@@ -331,7 +295,7 @@
@Override
public void delete(Change.Id id) throws IOException {
- Term idTerm = QueryBuilder.idTerm(schema, id);
+ Term idTerm = QueryBuilder.idTerm(id);
try {
Futures.allAsList(
openIndex.delete(idTerm),
@@ -358,8 +322,7 @@
if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
indexes.add(closedIndex);
}
- return new QuerySource(indexes, queryBuilder.toQuery(p), opts,
- getSort());
+ return new QuerySource(indexes, queryBuilder.toQuery(p), opts, getSort());
}
@Override
@@ -367,19 +330,10 @@
setReady(sitePaths, schema.getVersion(), ready);
}
- @SuppressWarnings("deprecation")
private Sort getSort() {
- if (useDocValuesForSorting) {
- return new Sort(
- new SortField(UPDATED_SORT_FIELD, SortField.Type.LONG, true),
- new SortField(idSortField, SortField.Type.LONG, true));
- } else {
- return new Sort(
- new SortField(
- ChangeField.UPDATED.getName(), SortField.Type.LONG, true),
- new SortField(
- ChangeField.LEGACY_ID.getName(), SortField.Type.INT, true));
- }
+ return new Sort(
+ new SortField(UPDATED_SORT_FIELD, SortField.Type.LONG, true),
+ new SortField(ID_SORT_FIELD, SortField.Type.LONG, true));
}
public SubIndex getOpenChangesIndex() {
@@ -434,7 +388,7 @@
List<ChangeData> result =
Lists.newArrayListWithCapacity(docs.scoreDocs.length);
Set<String> fields = fields(opts);
- String idFieldName = idFieldName();
+ String idFieldName = LEGACY_ID.getName();
for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
ScoreDoc sd = docs.scoreDocs[i];
Document doc = searchers[sd.shardIndex].doc(sd.doc, fields);
@@ -474,16 +428,14 @@
}
}
- @SuppressWarnings("deprecation")
private Set<String> fields(QueryOptions opts) {
- if (schemaHasRequestedField(ChangeField.LEGACY_ID2, opts.fields())
- || schemaHasRequestedField(ChangeField.CHANGE, opts.fields())
- || schemaHasRequestedField(ChangeField.LEGACY_ID, opts.fields())) {
+ if (schemaHasRequestedField(ChangeField.LEGACY_ID, opts.fields())
+ || schemaHasRequestedField(ChangeField.CHANGE, opts.fields())) {
return opts.fields();
}
// Request the numeric ID field even if the caller did not request it,
// otherwise we can't actually construct a ChangeData.
- return Sets.union(opts.fields(), ImmutableSet.of(idFieldName()));
+ return Sets.union(opts.fields(), ImmutableSet.of(LEGACY_ID.getName()));
}
private boolean schemaHasRequestedField(FieldDef<ChangeData, ?> field,
@@ -491,12 +443,6 @@
return schema.hasField(field) && requested.contains(field.getName());
}
- @SuppressWarnings("deprecation")
- private String idFieldName() {
- return schema.getField(ChangeField.LEGACY_ID2, ChangeField.LEGACY_ID).get()
- .getName();
- }
-
private ChangeData toChangeData(Document doc, Set<String> fields,
String idFieldName) {
ChangeData cd;
@@ -602,21 +548,20 @@
return result;
}
- @SuppressWarnings("deprecation")
private void add(Document doc, Values<ChangeData> values) {
String name = values.getField().getName();
FieldType<?> type = values.getField().getType();
Store store = store(values.getField());
- if (useDocValuesForSorting) {
- FieldDef<ChangeData, ?> f = values.getField();
- if (f == ChangeField.LEGACY_ID || f == ChangeField.LEGACY_ID2) {
- int v = (Integer) getOnlyElement(values.getValues());
- doc.add(new NumericDocValuesField(sortFieldName(f), v));
- } else if (f == ChangeField.UPDATED) {
- long t = ((Timestamp) getOnlyElement(values.getValues())).getTime();
- doc.add(new NumericDocValuesField(UPDATED_SORT_FIELD, t));
- }
+ FieldDef<ChangeData, ?> f = values.getField();
+
+ // Add separate DocValues fields for those fields needed for sorting.
+ if (f == ChangeField.LEGACY_ID) {
+ int v = (Integer) getOnlyElement(values.getValues());
+ doc.add(new NumericDocValuesField(sortFieldName(f), v));
+ } else if (f == ChangeField.UPDATED) {
+ long t = ((Timestamp) getOnlyElement(values.getValues())).getTime();
+ doc.add(new NumericDocValuesField(UPDATED_SORT_FIELD, t));
}
if (type == FieldType.INTEGER || type == FieldType.INTEGER_RANGE) {
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
index 939c1fb..7f26d83 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/QueryBuilder.java
@@ -14,7 +14,7 @@
package com.google.gerrit.lucene;
-import static com.google.gerrit.server.query.change.LegacyChangeIdPredicate.idField;
+import static com.google.gerrit.server.index.ChangeField.LEGACY_ID;
import static org.apache.lucene.search.BooleanClause.Occur.MUST;
import static org.apache.lucene.search.BooleanClause.Occur.MUST_NOT;
import static org.apache.lucene.search.BooleanClause.Occur.SHOULD;
@@ -25,7 +25,6 @@
import com.google.gerrit.server.index.IndexPredicate;
import com.google.gerrit.server.index.IntegerRangePredicate;
import com.google.gerrit.server.index.RegexPredicate;
-import com.google.gerrit.server.index.Schema;
import com.google.gerrit.server.index.TimestampRangePredicate;
import com.google.gerrit.server.query.AndPredicate;
import com.google.gerrit.server.query.NotPredicate;
@@ -51,12 +50,12 @@
public class QueryBuilder {
- public static Term idTerm(Schema<ChangeData> schema, ChangeData cd) {
- return intTerm(idField(schema).getName(), cd.getId().get());
+ public static Term idTerm(ChangeData cd) {
+ return intTerm(LEGACY_ID.getName(), cd.getId().get());
}
- public static Term idTerm(Schema<ChangeData> schema, Change.Id id) {
- return intTerm(idField(schema).getName(), id.get());
+ public static Term idTerm(Change.Id id) {
+ return intTerm(LEGACY_ID.getName(), id.get());
}
private final org.apache.lucene.util.QueryBuilder queryBuilder;
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
index bb69533bf..ac43363 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/SubIndex.java
@@ -243,6 +243,9 @@
} else if (isGenAvailableNowForCurrentSearcher()) {
set(null);
return true;
+ } else if (!reopenThread.isAlive()) {
+ setException(new IllegalStateException("NRT thread is dead"));
+ return true;
}
return false;
}
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 8d5d4b9..33c6e34 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
@@ -125,18 +125,33 @@
try {
String claimedIdentifier = user.getClaimedIdentity();
Account.Id actualId = accountManager.lookup(user.getExternalId());
- // Use case 1: claimed identity was provided during handshake phase
+ Account.Id claimedId = null;
+
+ // We try to retrieve claimed identity.
+ // For some reason, for example staging instance
+ // it may deviate from the really old OpenID identity.
+ // What we want to avoid in any event is to create new
+ // account instead of linking to the existing one.
+ // That why we query it here, not to lose linking mode.
if (!Strings.isNullOrEmpty(claimedIdentifier)) {
- log.debug("Claimed identity is set");
- Account.Id claimedId = accountManager.lookup(claimedIdentifier);
- if (claimedId != null && actualId != null) {
+ claimedId = accountManager.lookup(claimedIdentifier);
+ if (claimedId == null) {
+ log.debug("Claimed identity is unknown");
+ }
+ }
+
+ // Use case 1: claimed identity was provided during handshake phase
+ // and user account exists for this identity
+ if (claimedId != null) {
+ log.debug("Claimed identity is set and is known");
+ if (actualId != null) {
if (claimedId.equals(actualId)) {
// Both link to the same account, that's what we expected.
log.debug("Both link to the same account. All is fine.");
} else {
// This is (for now) a fatal error. There are two records
- // for what might be the same user.
- //
+ // for what might be the same user. The admin would have to
+ // link the accounts manually.
log.error("OAuth accounts disagree over user identity:\n"
+ " Claimed ID: " + claimedId + " is " + claimedIdentifier
+ "\n" + " Delgate ID: " + actualId + " is "
@@ -144,7 +159,7 @@
rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
- } else if (claimedId != null && actualId == null) {
+ } else {
// Claimed account already exists: link to it.
log.debug("Claimed account already exists: link to it.");
try {
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 2815099..5108315 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
@@ -333,6 +333,11 @@
modules.add(SchemaVersionCheck.module());
modules.add(new DropWizardMetricMaker.RestModule());
modules.add(new LogFileCompressor.Module());
+
+ // Index module shutdown must happen before work queue shutdown, otherwise
+ // work queue can get stuck waiting on index futures that will never return.
+ modules.add(createIndexModule());
+
modules.add(new WorkQueue.Module());
modules.add(new ChangeHookRunner.Module());
modules.add(new ReceiveCommitsExecutorModule());
@@ -351,7 +356,6 @@
modules.add(new PluginRestApiModule());
modules.add(new RestCacheAdminModule());
modules.add(new GpgModule(config));
- modules.add(createIndexModule());
if (MoreObjects.firstNonNull(httpd, true)) {
modules.add(new CanonicalWebUrlModule() {
@Override
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK
index b8fb4d2..07847fc 100644
--- a/gerrit-server/BUCK
+++ b/gerrit-server/BUCK
@@ -63,9 +63,9 @@
'//lib/log:api',
'//lib/log:jsonevent-layout',
'//lib/log:log4j',
- '//lib/lucene:analyzers-common',
- '//lib/lucene:core-and-backward-codecs',
- '//lib/lucene:queryparser',
+ '//lib/lucene:lucene-analyzers-common',
+ '//lib/lucene:lucene-core',
+ '//lib/lucene:lucene-queryparser',
'//lib/ow2:ow2-asm',
'//lib/ow2:ow2-asm-tree',
'//lib/ow2:ow2-asm-util',
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
index 629cf96..e7ab75c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCallback.java
@@ -38,6 +38,7 @@
private final V zero;
private final Map<Object, ValueGauge> cells;
protected volatile Runnable trigger;
+ private final Object lock = new Object();
BucketedCallback(DropWizardMetricMaker metrics, MetricRegistry registry,
String name, Class<V> valueType, Description desc, Field<?>... fields) {
@@ -94,7 +95,7 @@
return c;
}
- synchronized (cells) {
+ synchronized (lock) {
c = cells.get(key);
if (c == null) {
c = new ValueGauge();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCounter.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCounter.java
index 22af5ca..10b92e6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCounter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedCounter.java
@@ -35,6 +35,7 @@
protected final Field<?>[] fields;
protected final CounterImpl total;
private final Map<Object, CounterImpl> cells;
+ private final Object lock = new Object();
BucketedCounter(DropWizardMetricMaker metrics,
String name, Description desc, Field<?>... fields) {
@@ -69,7 +70,7 @@
return c;
}
- synchronized (cells) {
+ synchronized (lock) {
c = cells.get(key);
if (c == null) {
c = metrics.newCounterImpl(submetric(key), isRate);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedHistogram.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedHistogram.java
index 51c5fea..071c678 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedHistogram.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedHistogram.java
@@ -34,6 +34,7 @@
protected final Field<?>[] fields;
protected final HistogramImpl total;
private final Map<Object, HistogramImpl> cells;
+ private final Object lock = new Object();
BucketedHistogram(DropWizardMetricMaker metrics, String name,
Description desc, Field<?>... fields) {
@@ -67,7 +68,7 @@
return c;
}
- synchronized (cells) {
+ synchronized (lock) {
c = cells.get(key);
if (c == null) {
c = metrics.newHistogramImpl(submetric(key));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java
index ec12e00..6981ef1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/BucketedTimer.java
@@ -34,6 +34,7 @@
protected final Field<?>[] fields;
protected final TimerImpl total;
private final Map<Object, TimerImpl> cells;
+ private final Object lock = new Object();
BucketedTimer(DropWizardMetricMaker metrics, String name,
Description desc, Field<?>... fields) {
@@ -67,7 +68,7 @@
return c;
}
- synchronized (cells) {
+ synchronized (lock) {
c = cells.get(key);
if (c == null) {
c = metrics.newTimerImpl(submetric(key));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
index 93a3814..04546aa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
@@ -93,40 +93,32 @@
throws ResourceNotFoundException, ResourceConflictException, IOException {
Map<String, ProjectAccessInfo> access = Maps.newTreeMap();
for (String p: projects) {
+ // Load the current configuration from the repository, ensuring it's the most
+ // recent version available. If it differs from what was in the project
+ // state, force a cache flush now.
+ //
Project.NameKey projectName = new Project.NameKey(p);
- ProjectControl pc = open(projectName);
- ProjectConfig config;
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
+ ProjectControl pc = open(projectName);
+ ProjectConfig config = ProjectConfig.read(md);
- try {
- // Load the current configuration from the repository, ensuring it's the most
- // recent version available. If it differs from what was in the project
- // state, force a cache flush now.
- //
- MetaDataUpdate md = metaDataUpdateFactory.create(projectName);
- try {
- config = ProjectConfig.read(md);
-
- if (config.updateGroupNames(groupBackend)) {
- md.setMessage("Update group names\n");
- config.commit(md);
- projectCache.evict(config.getProject());
- pc = open(projectName);
- } else if (config.getRevision() != null
- && !config.getRevision().equals(
- pc.getProjectState().getConfig().getRevision())) {
- projectCache.evict(config.getProject());
- pc = open(projectName);
- }
- } catch (ConfigInvalidException e) {
- throw new ResourceConflictException(e.getMessage());
- } finally {
- md.close();
+ if (config.updateGroupNames(groupBackend)) {
+ md.setMessage("Update group names\n");
+ config.commit(md);
+ projectCache.evict(config.getProject());
+ pc = open(projectName);
+ } else if (config.getRevision() != null
+ && !config.getRevision().equals(
+ pc.getProjectState().getConfig().getRevision())) {
+ projectCache.evict(config.getProject());
+ pc = open(projectName);
}
+ access.put(p, new ProjectAccessInfo(pc, config));
+ } catch (ConfigInvalidException e) {
+ throw new ResourceConflictException(e.getMessage());
} catch (RepositoryNotFoundException e) {
throw new ResourceNotFoundException(p);
}
-
- access.put(p, new ProjectAccessInfo(pc, config));
}
return access;
}
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 35ab3af..c585f97 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,6 +14,7 @@
package com.google.gerrit.server.account;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_EXTERNAL;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
@@ -38,6 +39,15 @@
return r;
}
+ /** Create a request for an external username. */
+ public static AuthRequest forExternalUser(String username) {
+ AccountExternalId.Key i =
+ new AccountExternalId.Key(SCHEME_EXTERNAL, username);
+ AuthRequest r = new AuthRequest(i.get());
+ r.setUserName(username);
+ return r;
+ }
+
/**
* Create a request for an email address registration.
* <p>
@@ -58,6 +68,8 @@
private String emailAddress;
private String userName;
private boolean skipAuthentication;
+ private String authPlugin;
+ private String authProvider;
public AuthRequest(final String externalId) {
this.externalId = externalId;
@@ -125,4 +137,20 @@
public void setSkipAuthentication(boolean skip) {
skipAuthentication = skip;
}
+
+ public String getAuthPlugin() {
+ return authPlugin;
+ }
+
+ public void setAuthPlugin(String authPlugin) {
+ this.authPlugin = authPlugin;
+ }
+
+ public String getAuthProvider() {
+ return authProvider;
+ }
+
+ public void setAuthProvider(String authProvider) {
+ this.authProvider = authProvider;
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
index e14791c..9617d94 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
@@ -77,10 +77,8 @@
private DiffPreferencesInfo writeToGit(DiffPreferencesInfo in,
Account.Id userId) throws RepositoryNotFoundException, IOException,
ConfigInvalidException {
- MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName);
-
DiffPreferencesInfo out = new DiffPreferencesInfo();
- try {
+ try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
VersionedAccountPreferences prefs = VersionedAccountPreferences.forUser(
userId);
prefs.load(md);
@@ -90,8 +88,6 @@
prefs.commit(md);
loadSection(prefs.getConfig(), UserConfigSections.DIFF, null, out,
DiffPreferencesInfo.defaults(), null);
- } finally {
- md.close();
}
return out;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
index eabe31d..d51e24a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
@@ -71,18 +71,15 @@
}
Account.Id accountId = rsrc.getUser().getAccountId();
- MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName);
VersionedAccountPreferences prefs;
- try {
+ try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
prefs = VersionedAccountPreferences.forUser(accountId);
prefs.load(md);
storeSection(prefs.getConfig(), UserConfigSections.EDIT, null,
readFromGit(accountId, gitMgr, allUsersName, in),
EditPreferencesInfo.defaults());
prefs.commit(md);
- } finally {
- md.close();
}
return Response.none();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
index 7042076..51c54ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -115,9 +115,8 @@
Account.Id accountId = rsrc.getUser().getAccountId();
AccountGeneralPreferences p;
VersionedAccountPreferences versionedPrefs;
- MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName);
db.get().accounts().beginTransaction(accountId);
- try {
+ try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
Account a = db.get().accounts().get(accountId);
if (a == null) {
throw new ResourceNotFoundException();
@@ -184,7 +183,6 @@
return new GetPreferences.PreferenceInfo(
p, versionedPrefs, md.getRepository());
} finally {
- md.close();
db.get().rollback();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
index 7be8299..3bd7634 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -61,6 +61,11 @@
}
@Override
+ public AccountApi id(int id) throws RestApiException {
+ return id(String.valueOf(id));
+ }
+
+ @Override
public AccountApi self() throws RestApiException {
if (!self.get().isIdentifiedUser()) {
throw new AuthException("Authentication required");
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 0ca43f5..eb16ffb 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
@@ -27,10 +27,12 @@
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.changes.SubmitInput;
+import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ActionInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.extensions.common.MergeableInfo;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -54,6 +56,7 @@
import com.google.gerrit.server.change.Reviewed;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.Submit;
+import com.google.gerrit.server.change.TestSubmitType;
import com.google.gerrit.server.git.UpdateException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -94,6 +97,8 @@
private final Comments comments;
private final CommentApiImpl.Factory commentFactory;
private final GetRevisionActions revisionActions;
+ private final Provider<TestSubmitType> testSubmitType;
+ private final TestSubmitType.Get getSubmitType;
@Inject
RevisionApiImpl(Changes changes,
@@ -119,6 +124,8 @@
Comments comments,
CommentApiImpl.Factory commentFactory,
GetRevisionActions revisionActions,
+ Provider<TestSubmitType> testSubmitType,
+ TestSubmitType.Get getSubmitType,
@Assisted RevisionResource r) {
this.changes = changes;
this.cherryPick = cherryPick;
@@ -143,6 +150,8 @@
this.comments = comments;
this.commentFactory = commentFactory;
this.revisionActions = revisionActions;
+ this.testSubmitType = testSubmitType;
+ this.getSubmitType = getSubmitType;
this.revision = r;
}
@@ -375,4 +384,23 @@
public Map<String, ActionInfo> actions() throws RestApiException {
return revisionActions.apply(revision).value();
}
+
+ @Override
+ public SubmitType submitType() throws RestApiException {
+ try {
+ return getSubmitType.apply(revision);
+ } catch (OrmException e) {
+ throw new RestApiException("Cannot get submit type", e);
+ }
+ }
+
+ @Override
+ public SubmitType testSubmitType(TestSubmitRuleInput in)
+ throws RestApiException {
+ try {
+ return testSubmitType.get().apply(revision, in);
+ } catch (OrmException e) {
+ throw new RestApiException("Cannot test submit type", e);
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
new file mode 100644
index 0000000..582cc38
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth.oauth;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
+import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AbstractRealm;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+@Singleton
+public class OAuthRealm extends AbstractRealm {
+ private final DynamicMap<OAuthLoginProvider> loginProviders;
+
+ @Inject
+ OAuthRealm(DynamicMap<OAuthLoginProvider> loginProviders) {
+ this.loginProviders = loginProviders;
+ }
+
+ @Override
+ public boolean allowsEdit(FieldName field) {
+ return false;
+ }
+
+ /**
+ * Authenticates with the {@link OAuthLoginProvider} specified
+ * in the authentication request.
+ *
+ * {@link AccountManager} calls this method without password
+ * if authenticity of the user has already been established.
+ * In that case the {@link AuthRequest} is supposed to contain
+ * a resolved email address and we can skip the authentication
+ * request to the {@code OAuthLoginService}.
+ *
+ * @param who the authentication request.
+ *
+ * @return the authentication request with resolved email address
+ * and display name in case the authenticity of the user could
+ * be established; otherwise {@code who} is returned unchanged.
+ *
+ * @throws AccountException if the authentication request with
+ * the OAuth2 server failed or no {@code OAuthLoginProvider} was
+ * available to handle the request.
+ */
+ @Override
+ public AuthRequest authenticate(AuthRequest who) throws AccountException {
+ if (Strings.isNullOrEmpty(who.getPassword()) &&
+ !Strings.isNullOrEmpty(who.getEmailAddress())) {
+ return who;
+ }
+
+ if (Strings.isNullOrEmpty(who.getAuthPlugin())
+ || Strings.isNullOrEmpty(who.getAuthProvider())) {
+ throw new AccountException("Cannot authenticate");
+ }
+ OAuthLoginProvider loginProvider =
+ loginProviders.get(who.getAuthPlugin(), who.getAuthProvider());
+ if (loginProvider == null) {
+ throw new AccountException("Cannot authenticate");
+ }
+
+ OAuthUserInfo userInfo;
+ try {
+ userInfo = loginProvider.login(who.getUserName(), who.getPassword());
+ } catch (IOException e) {
+ throw new AccountException("Cannot authenticate", e);
+ }
+ if (userInfo == null) {
+ throw new AccountException("Cannot authenticate");
+ }
+ if (!Strings.isNullOrEmpty(userInfo.getEmailAddress())) {
+ who.setEmailAddress(userInfo.getEmailAddress());
+ }
+ if (!Strings.isNullOrEmpty(userInfo.getDisplayName())) {
+ who.setDisplayName(userInfo.getDisplayName());
+ }
+ return who;
+ }
+
+ @Override
+ public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who) {
+ return who;
+ }
+
+ @Override
+ public AuthRequest unlink(ReviewDb db, Account.Id to, AuthRequest who)
+ throws AccountException {
+ return who;
+ }
+
+ @Override
+ public void onCreateAccount(AuthRequest who, Account account) {
+ }
+
+ @Override
+ public Account.Id lookup(String accountName) {
+ return null;
+ }
+}
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 1217f34..c796e6a 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
@@ -84,7 +84,6 @@
private final RefControl refControl;
private final IdentifiedUser user;
- private final Change change;
private final PatchSet patchSet;
private final RevCommit commit;
@@ -101,6 +100,7 @@
private boolean updateRef;
// Fields set during the insertion process.
+ private Change change;
private ChangeMessage changeMessage;
private PatchSetInfo patchSetInfo;
@@ -230,9 +230,6 @@
public void updateRepo(RepoContext ctx)
throws ResourceConflictException, IOException {
validate(ctx);
- patchSetInfo = patchSetInfoFactory.get(
- ctx.getRevWalk(), commit, patchSet.getId());
- change.setCurrentPatchSet(patchSetInfo);
if (!updateRef) {
return;
}
@@ -242,9 +239,14 @@
@Override
public void updateChange(ChangeContext ctx) throws OrmException, IOException {
+ change = ctx.getChange(); // Use defensive copy created by ChangeControl.
ReviewDb db = ctx.getDb();
ChangeControl ctl = ctx.getChangeControl();
ChangeUpdate update = ctx.getChangeUpdate();
+ patchSetInfo = patchSetInfoFactory.get(
+ ctx.getRevWalk(), commit, patchSet.getId());
+ ctx.getChange().setCurrentPatchSet(patchSetInfo);
+
if (patchSet.getGroups() == null) {
patchSet.setGroups(GroupCollector.getDefaultGroups(patchSet));
}
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 7d64591..da5f9be 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
@@ -58,6 +58,7 @@
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.extensions.api.changes.FixInput;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.AccountInfo;
@@ -403,10 +404,13 @@
out.topic = in.getTopic();
out.hashtags = ctl.getNotes().load().getHashtags();
out.changeId = in.getKey().get();
- // TODO(dborowitz): This gets the submit type, so we could include that in
- // the response and avoid making a request to /submit_type from the UI.
- out.mergeable = in.getStatus() == Change.Status.MERGED
- ? null : cd.isMergeable();
+ if (in.getStatus() != Change.Status.MERGED) {
+ SubmitTypeRecord str = cd.submitTypeRecord();
+ if (str.isOk()) {
+ out.submitType = str.type;
+ }
+ out.mergeable = cd.isMergeable();
+ }
out.submittable = Submit.submittable(cd);
ChangedLines changedLines = cd.changedLines();
if (changedLines != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
index 7392cdb..0d685df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -106,13 +106,13 @@
if (a.getLabel().equals(label)) {
msg.append("Removed ")
.append(a.getLabel()).append(formatLabelValue(a.getValue()))
- .append(" by ").append(userFactory.create(user.getAccountId())
+ .append(" by ").append(userFactory.create(a.getAccountId())
.getNameEmail())
.append("\n");
psa = a;
a.setValue((short)0);
ctx.getChangeUpdate().setPatchSetId(psId);
- ctx.getChangeUpdate().removeApproval(label);
+ ctx.getChangeUpdate().removeApprovalFor(a.getAccountId(), label);
break;
}
} else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
index c099e43..e83b539 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCache.java
@@ -16,7 +16,6 @@
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.server.ReviewDb;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -27,8 +26,7 @@
public static class NotImplemented implements MergeabilityCache {
@Override
public boolean get(ObjectId commit, Ref intoRef, SubmitType submitType,
- String mergeStrategy, Branch.NameKey dest, Repository repo,
- ReviewDb db) {
+ String mergeStrategy, Branch.NameKey dest, Repository repo) {
throw new UnsupportedOperationException("Mergeability checking disabled");
}
@@ -40,7 +38,7 @@
}
boolean get(ObjectId commit, Ref intoRef, SubmitType submitType,
- String mergeStrategy, Branch.NameKey dest, Repository repo, ReviewDb db);
+ String mergeStrategy, Branch.NameKey dest, Repository repo);
Boolean getIfPresent(ObjectId commit, Ref intoRef,
SubmitType submitType, String mergeStrategy);
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 826ebdd..2dacef9 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
@@ -26,32 +26,23 @@
import com.google.common.cache.Weigher;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Sets;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.server.ReviewDb;
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.SubmitStrategyFactory;
+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;
import com.google.inject.name.Named;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -188,13 +179,11 @@
private final EntryKey key;
private final Branch.NameKey dest;
private final Repository repo;
- private final ReviewDb db;
- Loader(EntryKey key, Branch.NameKey dest, Repository repo, ReviewDb db) {
+ Loader(EntryKey key, Branch.NameKey dest, Repository repo) {
this.key = key;
this.dest = dest;
this.repo = repo;
- this.db = db;
}
@Override
@@ -203,43 +192,14 @@
if (key.into.equals(ObjectId.zeroId())) {
return true; // Assume yes on new branch.
}
- RefDatabase refDatabase = repo.getRefDatabase();
- Iterable<Ref> refs = Iterables.concat(
- refDatabase.getRefs(Constants.R_HEADS).values(),
- refDatabase.getRefs(Constants.R_TAGS).values());
try (CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
- RevFlag canMerge = rw.newFlag("CAN_MERGE");
- CodeReviewCommit rev = rw.parseCommit(key.commit);
- rev.add(canMerge);
- CodeReviewCommit tip = rw.parseCommit(key.into);
- Set<RevCommit> accepted = alreadyAccepted(rw, refs);
- accepted.add(tip);
- accepted.addAll(Arrays.asList(rev.getParents()));
- return submitStrategyFactory.create(
- key.submitType,
- db,
- repo,
- rw,
- null /*inserter*/,
- canMerge,
- accepted,
- dest,
- null).dryRun(tip, rev);
+ 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);
}
}
-
- private Set<RevCommit> alreadyAccepted(RevWalk rw, Iterable<Ref> refs)
- throws MissingObjectException, IOException {
- Set<RevCommit> accepted = Sets.newHashSet();
- for (Ref r : refs) {
- try {
- accepted.add(rw.parseCommit(r.getObjectId()));
- } catch (IncorrectObjectTypeException nonCommit) {
- // Not a commit? Skip over it.
- }
- }
- return accepted;
- }
}
public static class MergeabilityWeigher
@@ -251,24 +211,24 @@
}
}
- private final SubmitStrategyFactory submitStrategyFactory;
+ private final SubmitDryRun submitDryRun;
private final Cache<EntryKey, Boolean> cache;
@Inject
MergeabilityCacheImpl(
- SubmitStrategyFactory submitStrategyFactory,
+ SubmitDryRun submitDryRun,
@Named(CACHE_NAME) Cache<EntryKey, Boolean> cache) {
- this.submitStrategyFactory = submitStrategyFactory;
+ this.submitDryRun = submitDryRun;
this.cache = cache;
}
@Override
public boolean get(ObjectId commit, Ref intoRef, SubmitType submitType,
- String mergeStrategy, Branch.NameKey dest, Repository repo, ReviewDb db) {
+ String mergeStrategy, Branch.NameKey dest, Repository repo) {
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, db));
+ return cache.get(key, new Loader(key, dest, repo));
} catch (ExecutionException e) {
log.error(String.format("Error checking mergeability of %s into %s (%s)",
key.commit.name(), key.into.name(), key.submitType.name()),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
index 007c233..4baaa63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -143,7 +143,7 @@
continue;
}
if (cache.get(commit, other, SubmitType.CHERRY_PICK, strategy,
- change.getDest(), git, db.get())) {
+ change.getDest(), git)) {
result.mergeableInto.add(other.getName().substring(prefixLen));
}
}
@@ -166,7 +166,7 @@
final Ref ref, SubmitType type, String strategy, Repository git,
Boolean old) throws OrmException, IOException {
final boolean mergeable =
- cache.get(commit, ref, type, strategy, change.getDest(), git, db.get());
+ cache.get(commit, ref, type, strategy, change.getDest(), git);
if (!Objects.equals(mergeable, old)) {
// TODO(dborowitz): Include cache info in ETag somehow instead.
ChangeUtil.bumpRowVersionNotLastUpdatedOn(change.getId(), db.get());
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 9a72b07..8628316 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
@@ -200,14 +200,13 @@
throws ResourceConflictException, IOException {
init();
validate(ctx);
- patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId);
ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(),
commit, getPatchSetId().toRefName(), ReceiveCommand.Type.CREATE));
}
@Override
public void updateChange(ChangeContext ctx) throws OrmException,
- InvalidChangeOperationException {
+ InvalidChangeOperationException, IOException {
ChangeControl ctl = ctx.getChangeControl();
change = ctx.getChange();
@@ -242,6 +241,7 @@
changeMessage.setMessage(message);
}
+ patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId);
// TODO(dborowitz): Throw ResourceConflictException instead of using
// AtomicUpdate.
change = db.changes().atomicUpdate(id, new AtomicUpdate<Change>() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index 9f2a3f9..efc98fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -42,7 +42,6 @@
import com.google.gerrit.server.git.BatchUpdate;
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.BatchUpdate.Context;
-import com.google.gerrit.server.git.BatchUpdate.RepoContext;
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.mail.CreateChangeSender;
import com.google.gerrit.server.mail.MailUtil.MailRecipients;
@@ -155,7 +154,6 @@
private PatchSet patchSet;
private Change change;
private boolean wasDraftChange;
- private RevCommit commit;
private PatchSetInfo patchSetInfo;
private MailRecipients recipients;
@@ -165,28 +163,17 @@
}
@Override
- public void updateRepo(RepoContext ctx)
- throws RestApiException, OrmException, IOException {
- PatchSet ps = patchSet;
- if (ps == null) {
- // Don't save in patchSet, since we're not in a transaction. Here we
- // just need the revision, which is immutable.
- ps = ctx.getDb().patchSets().get(psId);
- if (ps == null) {
- throw new ResourceNotFoundException(psId.toString());
- }
- }
- commit = ctx.getRevWalk().parseCommit(
- ObjectId.fromString(ps.getRevision().get()));
- patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId);
- }
-
- @Override
public void updateChange(ChangeContext ctx)
- throws RestApiException, OrmException {
+ throws RestApiException, OrmException, IOException {
if (!ctx.getChangeControl().canPublish(ctx.getDb())) {
throw new AuthException("Cannot publish this draft patch set");
}
+ if (patchSet == null) {
+ patchSet = ctx.getDb().patchSets().get(psId);
+ if (patchSet == null) {
+ throw new ResourceNotFoundException(psId.toString());
+ }
+ }
saveChange(ctx);
savePatchSet(ctx);
addReviewers(ctx);
@@ -204,7 +191,6 @@
private void savePatchSet(ChangeContext ctx)
throws RestApiException, OrmException {
- patchSet = ctx.getDb().patchSets().get(psId);
if (!patchSet.isDraft()) {
throw new ResourceConflictException("Patch set is not a draft");
}
@@ -217,10 +203,15 @@
ctx.getDb().patchSets().update(Collections.singleton(patchSet));
}
- private void addReviewers(ChangeContext ctx) throws OrmException {
+ private void addReviewers(ChangeContext ctx)
+ throws OrmException, IOException {
LabelTypes labelTypes = ctx.getChangeControl().getLabelTypes();
Collection<Account.Id> oldReviewers = approvalsUtil.getReviewers(
ctx.getDb(), ctx.getChangeNotes()).values();
+ RevCommit commit = ctx.getRevWalk().parseCommit(
+ ObjectId.fromString(patchSet.getRevision().get()));
+ patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId);
+
List<FooterLine> footerLines = commit.getFooterLines();
recipients =
getRecipientsFromFooters(accountResolver, patchSet, footerLines);
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 d3367a0..7848dfe 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
@@ -144,7 +144,7 @@
@Override
public void updateChange(ChangeContext ctx)
- throws OrmException, InvalidChangeOperationException {
+ throws OrmException, InvalidChangeOperationException, IOException {
patchSetInserter.updateChange(ctx);
rebasedPatchSet = patchSetInserter.getPatchSet();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java
index a0e641e..8cd82a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java
@@ -46,6 +46,7 @@
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -94,7 +95,7 @@
}
}
- List<PatchSetData> ancestors = walkAncestors(ctl, parents, start);
+ Collection<PatchSetData> ancestors = walkAncestors(ctl, parents, start);
List<PatchSetData> descendants =
walkDescendants(ctl, children, start, otherPatchSetsOfStart, ancestors);
List<PatchSetData> result =
@@ -127,15 +128,15 @@
return result;
}
- private static List<PatchSetData> walkAncestors(ProjectControl ctl,
+ private static Collection<PatchSetData> walkAncestors(ProjectControl ctl,
ListMultimap<PatchSetData, PatchSetData> parents, PatchSetData start)
throws OrmException {
- List<PatchSetData> result = new ArrayList<>();
+ LinkedHashSet<PatchSetData> result = new LinkedHashSet<>();
Deque<PatchSetData> pending = new ArrayDeque<>();
pending.add(start);
while (!pending.isEmpty()) {
PatchSetData psd = pending.remove();
- if (!isVisible(psd, ctl)) {
+ if (result.contains(psd) || !isVisible(psd, ctl)) {
continue;
}
result.add(psd);
@@ -147,7 +148,7 @@
private static List<PatchSetData> walkDescendants(ProjectControl ctl,
ListMultimap<PatchSetData, PatchSetData> children,
PatchSetData start, List<PatchSetData> otherPatchSetsOfStart,
- List<PatchSetData> ancestors)
+ Iterable<PatchSetData> ancestors)
throws OrmException {
Set<Change.Id> alreadyEmittedChanges = new HashSet<>();
addAllChangeIds(alreadyEmittedChanges, ancestors);
@@ -180,14 +181,16 @@
return ImmutableList.of();
}
Map<Change.Id, PatchSet.Id> maxPatchSetIds = new HashMap<>();
+ Set<PatchSetData> seen = new HashSet<>();
List<PatchSetData> allPatchSets = new ArrayList<>();
Deque<PatchSetData> pending = new ArrayDeque<>();
pending.addAll(start);
while (!pending.isEmpty()) {
PatchSetData psd = pending.remove();
- if (!isVisible(psd, ctl)) {
+ if (seen.contains(psd) || !isVisible(psd, ctl)) {
continue;
}
+ seen.add(psd);
if (!alreadyEmittedChanges.contains(psd.id())) {
// Don't emit anything for changes that were previously emitted, even
// though different patch sets might show up later. However, do
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index ec0f937..e24a290 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -110,7 +110,6 @@
PatchSet ps = cd.currentPatchSet();
if (ps != null) {
for (SubmitRecord rec : new SubmitRuleEvaluator(cd)
- .setPatchSet(ps)
.setFastEvalLabels(true)
.setAllowDraft(true)
.evaluate()) {
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 d22b1dd..c1f99be 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
@@ -182,9 +182,9 @@
rsrc.getPatchSet().getRevision().get()));
}
- try {
+ try (MergeOp op = mergeOpProvider.get()) {
ReviewDb db = dbProvider.get();
- mergeOpProvider.get().merge(db, change, caller, true);
+ op.merge(db, change, caller, true);
change = db.changes().get(change.getId());
} catch (NoSuchChangeException e) {
throw new OrmException("Submission failed", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
index 95a701e..3eecea3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
@@ -19,13 +19,13 @@
import com.google.common.collect.Maps;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput.Filters;
import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.rules.RulesCache;
import com.google.gerrit.server.account.AccountLoader;
-import com.google.gerrit.server.change.TestSubmitRule.Input;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
@@ -37,17 +37,8 @@
import java.util.List;
import java.util.Map;
-public class TestSubmitRule implements RestModifyView<RevisionResource, Input> {
- public enum Filters {
- RUN, SKIP
- }
-
- public static class Input {
- @DefaultInput
- public String rule;
- public Filters filters;
- }
-
+public class TestSubmitRule
+ implements RestModifyView<RevisionResource, TestSubmitRuleInput> {
private final Provider<ReviewDb> db;
private final ChangeData.Factory changeDataFactory;
private final RulesCache rules;
@@ -68,10 +59,10 @@
}
@Override
- public List<Record> apply(RevisionResource rsrc, Input input)
+ public List<Record> apply(RevisionResource rsrc, TestSubmitRuleInput input)
throws AuthException, OrmException {
if (input == null) {
- input = new Input();
+ input = new TestSubmitRuleInput();
}
if (input.rule != null && !rules.isProjectRulesEnabled()) {
throw new AuthException("project rules are disabled");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
index f6016b5..4855012 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
@@ -17,14 +17,14 @@
import com.google.common.base.MoreObjects;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput.Filters;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.rules.RulesCache;
-import com.google.gerrit.server.change.TestSubmitRule.Filters;
-import com.google.gerrit.server.change.TestSubmitRule.Input;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
@@ -33,7 +33,8 @@
import org.kohsuke.args4j.Option;
-public class TestSubmitType implements RestModifyView<RevisionResource, Input> {
+public class TestSubmitType
+ implements RestModifyView<RevisionResource, TestSubmitRuleInput> {
private final Provider<ReviewDb> db;
private final ChangeData.Factory changeDataFactory;
private final RulesCache rules;
@@ -51,10 +52,10 @@
}
@Override
- public SubmitType apply(RevisionResource rsrc, Input input)
+ public SubmitType apply(RevisionResource rsrc, TestSubmitRuleInput input)
throws AuthException, BadRequestException, OrmException {
if (input == null) {
- input = new Input();
+ input = new TestSubmitRuleInput();
}
if (input.rule != null && !rules.isProjectRulesEnabled()) {
throw new AuthException("project rules are disabled");
@@ -71,13 +72,13 @@
if (rec.status != SubmitTypeRecord.Status.OK) {
throw new BadRequestException(String.format(
"rule %s produced invalid result: %s",
- evaluator.getSubmitRule(), rec));
+ evaluator.getSubmitRuleName(), rec));
}
return rec.type;
}
- static class Get implements RestReadView<RevisionResource> {
+ public static class Get implements RestReadView<RevisionResource> {
private final TestSubmitType test;
@Inject
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
index ca4a9d2..a662328 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
@@ -21,6 +21,7 @@
import com.google.gerrit.server.auth.AuthBackend;
import com.google.gerrit.server.auth.InternalAuthBackend;
import com.google.gerrit.server.auth.ldap.LdapModule;
+import com.google.gerrit.server.auth.oauth.OAuthRealm;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
@@ -42,6 +43,10 @@
install(new LdapModule());
break;
+ case OAUTH:
+ bind(Realm.class).to(OAuthRealm.class);
+ break;
+
case CUSTOM_EXTENSION:
break;
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 6b93a68..564e0fb 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
@@ -19,6 +19,7 @@
import com.google.common.cache.Cache;
import com.google.gerrit.audit.AuditModule;
import com.google.gerrit.common.EventListener;
+import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.config.CloneCommand;
import com.google.gerrit.extensions.config.DownloadCommand;
@@ -296,6 +297,7 @@
DynamicSet.setOf(binder(), DiffWebLink.class);
DynamicSet.setOf(binder(), ProjectWebLink.class);
DynamicSet.setOf(binder(), BranchWebLink.class);
+ DynamicMap.mapOf(binder(), OAuthLoginProvider.class);
factory(UploadValidators.Factory.class);
DynamicSet.setOf(binder(), UploadValidationListener.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
index d6693ae..07d22b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
@@ -60,15 +60,12 @@
}
VersionedAccountPreferences p;
- MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName);
- try {
+ try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName)) {
p = VersionedAccountPreferences.forDefault();
p.load(md);
com.google.gerrit.server.account.SetPreferences.storeMyMenus(p, i.my);
p.commit(md);
return new PreferenceInfo(null, p, md.getRepository());
- } finally {
- md.close();
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index 90d1783..7a8cb9ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -102,6 +102,19 @@
}
public class Context {
+ private Repository repoWrapper;
+
+ public Repository getRepository() throws IOException {
+ if (repoWrapper == null) {
+ repoWrapper = new ReadOnlyRepository(BatchUpdate.this.getRepository());
+ }
+ return repoWrapper;
+ }
+
+ public RevWalk getRevWalk() throws IOException {
+ return BatchUpdate.this.getRevWalk();
+ }
+
public Project.NameKey getProject() {
return project;
}
@@ -124,19 +137,13 @@
}
public class RepoContext extends Context {
+ @Override
public Repository getRepository() throws IOException {
- initRepository();
- return repo;
- }
-
- public RevWalk getRevWalk() throws IOException {
- initRepository();
- return revWalk;
+ return BatchUpdate.this.getRepository();
}
public ObjectInserter getInserter() throws IOException {
- initRepository();
- return inserter;
+ return BatchUpdate.this.getObjectInserter();
}
public BatchRefUpdate getBatchRefUpdate() throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
index 37a0886..f07b922 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -20,13 +20,13 @@
import com.google.common.collect.Ordering;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.server.git.strategy.CommitMergeStatus;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.ChangeControl;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -63,33 +63,6 @@
return new CodeReviewRevWalk(reader);
}
- static CodeReviewCommit revisionGone(ChangeControl ctl) {
- return error(ctl, CommitMergeStatus.REVISION_GONE);
- }
-
- static CodeReviewCommit noPatchSet(ChangeControl ctl) {
- return error(ctl, CommitMergeStatus.NO_PATCH_SET);
- }
-
- /**
- * Create an error commit.
- * <p>
- * Should only be used for error statuses such that there is no possible
- * non-zero commit on which we could call {@link
- * #setStatusCode(CommitMergeStatus)}, enumerated in the methods above.
- *
- * @param ctl control for change that caused this error
- * @param s status
- * @return new commit instance
- */
- private static CodeReviewCommit error(ChangeControl ctl,
- CommitMergeStatus s) {
- CodeReviewCommit r = new CodeReviewCommit(ObjectId.zeroId());
- r.setControl(ctl);
- r.statusCode = s;
- return r;
- }
-
public static class CodeReviewRevWalk extends RevWalk {
private CodeReviewRevWalk(Repository repo) {
super(repo);
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 61689a1..1ede59c 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
@@ -16,17 +16,22 @@
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
-import static org.eclipse.jgit.lib.RefDatabase.ALL;
+import com.google.auto.value.AutoValue;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
-import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
-import com.google.common.collect.ListMultimap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
@@ -55,12 +60,12 @@
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
+import com.google.gerrit.server.git.strategy.CommitMergeStatus;
import com.google.gerrit.server.git.strategy.SubmitStrategy;
import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
import com.google.gerrit.server.git.validators.MergeValidationException;
import com.google.gerrit.server.git.validators.MergeValidators;
import com.google.gerrit.server.index.ChangeIndexer;
-import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
@@ -120,13 +125,83 @@
* marking it as conflicting, even if an earlier commit along that same line can
* be merged cleanly.
*/
-public class MergeOp {
+public class MergeOp implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
+ private static class OpenRepo {
+ final Repository repo;
+ final CodeReviewRevWalk rw;
+ final RevFlag canMergeFlag;
+ final ObjectInserter ins;
+ ProjectState project;
+
+ private final Map<Branch.NameKey, OpenBranch> branches;
+
+ OpenRepo(Repository repo, ProjectState project) {
+ this.repo = repo;
+ this.project = project;
+ rw = CodeReviewCommit.newRevWalk(repo);
+ rw.sort(RevSort.TOPO);
+ rw.sort(RevSort.COMMIT_TIME_DESC, true);
+ rw.setRetainBody(false);
+ canMergeFlag = rw.newFlag("CAN_MERGE");
+
+ ins = repo.newObjectInserter();
+ branches = Maps.newHashMapWithExpectedSize(1);
+ }
+
+ OpenBranch getBranch(Branch.NameKey branch) throws IntegrationException {
+ OpenBranch ob = branches.get(branch);
+ if (ob == null) {
+ ob = new OpenBranch(this, branch);
+ branches.put(branch, ob);
+ }
+ return ob;
+ }
+
+ Project.NameKey getProjectName() {
+ return project.getProject().getNameKey();
+ }
+
+ void close() {
+ ins.close();
+ rw.close();
+ repo.close();
+ }
+ }
+
+ private static class OpenBranch {
+ final Branch.NameKey name;
+ final RefUpdate update;
+ final CodeReviewCommit oldTip;
+ MergeTip mergeTip;
+
+ OpenBranch(OpenRepo or, Branch.NameKey name) throws IntegrationException {
+ this.name = name;
+ try {
+ update = or.repo.updateRef(name.get());
+ if (update.getOldObjectId() != null) {
+ oldTip = or.rw.parseCommit(update.getOldObjectId());
+ } else if (Objects.equals(or.repo.getFullBranch(), name.get())) {
+ oldTip = null;
+ update.setExpectedOldObjectId(ObjectId.zeroId());
+ } else {
+ throw new IntegrationException("The destination branch "
+ + name + " does not exist anymore.");
+ }
+ } catch (IOException e) {
+ throw new IntegrationException("Cannot open branch " + name, e);
+ }
+ }
+
+ CodeReviewCommit getCurrentTip() {
+ return mergeTip != null ? mergeTip.getCurrentTip() : oldTip;
+ }
+ }
+
private final AccountCache accountCache;
private final ApprovalsUtil approvalsUtil;
private final ChangeControl.GenericFactory changeControlFactory;
- private final ChangeData.Factory changeDataFactory;
private final ChangeHooks hooks;
private final ChangeIndexer indexer;
private final ChangeMessagesUtil cmUtil;
@@ -146,8 +221,9 @@
private final Provider<SubmoduleOp> subOpProvider;
private final TagCache tagCache;
- private final Map<Change.Id, List<SubmitRecord>> records;
+ private final Map<Project.NameKey, OpenRepo> openRepos;
private final Map<Change.Id, CodeReviewCommit> commits;
+ private final Multimap<Change.Id, String> problems;
private static final String MACHINE_ID;
static {
@@ -162,21 +238,12 @@
private String staticSubmissionId;
private String submissionId;
- private ProjectState destProject;
private ReviewDb db;
- private Repository repo;
- private CodeReviewRevWalk rw;
- private RevFlag canMergeFlag;
- private ObjectInserter inserter;
- private Map<Branch.NameKey, RefUpdate> pendingRefUpdates;
- private Map<Branch.NameKey, CodeReviewCommit> openBranches;
- private Map<Branch.NameKey, MergeTip> mergeTips;
@Inject
MergeOp(AccountCache accountCache,
ApprovalsUtil approvalsUtil,
ChangeControl.GenericFactory changeControlFactory,
- ChangeData.Factory changeDataFactory,
ChangeHooks hooks,
ChangeIndexer indexer,
ChangeMessagesUtil cmUtil,
@@ -198,7 +265,6 @@
this.accountCache = accountCache;
this.approvalsUtil = approvalsUtil;
this.changeControlFactory = changeControlFactory;
- this.changeDataFactory = changeDataFactory;
this.hooks = hooks;
this.indexer = indexer;
this.cmUtil = cmUtil;
@@ -219,23 +285,40 @@
this.tagCache = tagCache;
commits = new HashMap<>();
- pendingRefUpdates = new HashMap<>();
- openBranches = new HashMap<>();
- pendingRefUpdates = new HashMap<>();
- records = new HashMap<>();
- mergeTips = new HashMap<>();
+ openRepos = new HashMap<>();
+ problems = MultimapBuilder.linkedHashKeys().arrayListValues(1).build();
}
- private void setDestProject(Branch.NameKey destBranch)
- throws IntegrationException {
- destProject = projectCache.get(destBranch.getParentKey());
- if (destProject == null) {
- throw new IntegrationException(
- "No such project: " + destBranch.getParentKey());
+ private OpenRepo openRepo(Project.NameKey project)
+ throws NoSuchProjectException, IOException {
+ OpenRepo repo = openRepos.get(project);
+ if (repo == null) {
+ ProjectState projectState = projectCache.get(project);
+ if (projectState == null) {
+ throw new NoSuchProjectException(project);
+ }
+ try {
+ repo = new OpenRepo(repoManager.openRepository(project), projectState);
+ } catch (RepositoryNotFoundException e) {
+ throw new NoSuchProjectException(project);
+ }
+ openRepos.put(project, repo);
+ }
+ return repo;
+ }
+
+ @Override
+ public void close() {
+ for (OpenRepo repo : openRepos.values()) {
+ repo.close();
}
}
- private static Optional<SubmitRecord> findOkRecord(Collection<SubmitRecord> in) {
+ private static Optional<SubmitRecord> findOkRecord(
+ Collection<SubmitRecord> in) {
+ if (in == null) {
+ return Optional.absent();
+ }
return Iterables.tryFind(in, new Predicate<SubmitRecord>() {
@Override
public boolean apply(SubmitRecord input) {
@@ -244,20 +327,21 @@
});
}
- public static List<SubmitRecord> checkSubmitRule(ChangeData cd)
+ public static void checkSubmitRule(ChangeData cd)
throws ResourceConflictException, OrmException {
PatchSet patchSet = cd.currentPatchSet();
if (patchSet == null) {
throw new ResourceConflictException(
"missing current patch set for change " + cd.getId());
}
- List<SubmitRecord> results = new SubmitRuleEvaluator(cd)
- .setPatchSet(patchSet)
- .evaluate();
- Optional<SubmitRecord> ok = findOkRecord(results);
- if (ok.isPresent()) {
+ List<SubmitRecord> results = cd.getSubmitRecords();
+ if (results == null) {
+ results = new SubmitRuleEvaluator(cd).evaluate();
+ cd.setSubmitRecords(results);
+ }
+ if (findOkRecord(results).isPresent()) {
// Rules supplied a valid solution.
- return ImmutableList.of(ok.get());
+ return;
} else if (results.isEmpty()) {
throw new IllegalStateException(String.format(
"SubmitRuleEvaluator.evaluate for change %s " +
@@ -270,49 +354,15 @@
for (SubmitRecord record : results) {
switch (record.status) {
case CLOSED:
- throw new ResourceConflictException(String.format(
- "change %s is closed", cd.getId()));
+ throw new ResourceConflictException("change is closed");
case RULE_ERROR:
- throw new ResourceConflictException(String.format(
- "rule error for change %s: %s",
- cd.getId(), record.errorMessage));
+ throw new ResourceConflictException(
+ "submit rule error: " + record.errorMessage);
case NOT_READY:
- StringBuilder msg = new StringBuilder();
- msg.append(cd.getId() + ":");
- for (SubmitRecord.Label lbl : record.labels) {
- switch (lbl.status) {
- case OK:
- case MAY:
- continue;
-
- case REJECT:
- msg.append(" blocked by ").append(lbl.label);
- msg.append(";");
- continue;
-
- case NEED:
- msg.append(" needs ").append(lbl.label);
- msg.append(";");
- continue;
-
- case IMPOSSIBLE:
- msg.append(" needs ").append(lbl.label)
- .append(" (check project access)");
- msg.append(";");
- continue;
-
- default:
- throw new IllegalStateException(String.format(
- "Unsupported SubmitRecord.Label %s for %s in %s in %s",
- lbl.toString(),
- patchSet.getId(),
- cd.getId(),
- cd.change().getProject().get()));
- }
- }
- throw new ResourceConflictException(msg.toString());
+ throw new ResourceConflictException(
+ describeLabels(cd, record.labels));
default:
throw new IllegalStateException(String.format(
@@ -325,32 +375,55 @@
throw new IllegalStateException();
}
- private void checkSubmitRulesAndState(ChangeSet cs)
- throws ResourceConflictException, OrmException {
+ private static String describeLabels(ChangeData cd,
+ List<SubmitRecord.Label> labels) throws OrmException {
+ List<String> labelResults = new ArrayList<>();
+ for (SubmitRecord.Label lbl : labels) {
+ switch (lbl.status) {
+ case OK:
+ case MAY:
+ break;
- StringBuilder msgbuf = new StringBuilder();
- List<Change.Id> problemChanges = new ArrayList<>();
- for (Change.Id id : cs.ids()) {
- try {
- ChangeData cd = changeDataFactory.create(db, id);
- if (cd.change().getStatus() != Change.Status.NEW){
- throw new ResourceConflictException("Change " +
- cd.change().getChangeId() + " is in state " +
- cd.change().getStatus());
- } else {
- records.put(cd.change().getId(), checkSubmitRule(cd));
- }
- } catch (ResourceConflictException e) {
- msgbuf.append(e.getMessage() + "\n");
- problemChanges.add(id);
+ case REJECT:
+ labelResults.add("blocked by " + lbl.label);
+ break;
+
+ case NEED:
+ labelResults.add("needs " + lbl.label);
+ break;
+
+ case IMPOSSIBLE:
+ labelResults.add(
+ "needs " + lbl.label + " (check project access)");
+ break;
+
+ default:
+ throw new IllegalStateException(String.format(
+ "Unsupported SubmitRecord.Label %s for %s in %s",
+ lbl,
+ cd.change().currentPatchSetId(),
+ cd.change().getProject()));
}
}
- String reason = msgbuf.toString();
- if (!reason.isEmpty()) {
- throw new ResourceConflictException("The change could not be " +
- "submitted because it depends on change(s) " +
- problemChanges.toString() + ", which could not be submitted " +
- "because:\n" + reason);
+ return Joiner.on("; ").join(labelResults);
+ }
+
+ private void checkSubmitRulesAndState(ChangeSet cs) {
+ for (ChangeData cd : cs.changes()) {
+ try {
+ if (cd.change().getStatus() != Change.Status.NEW) {
+ problems.put(cd.getId(), "Change " + cd.getId() + " is "
+ + cd.change().getStatus().toString().toLowerCase());
+ } else {
+ checkSubmitRule(cd);
+ }
+ } catch (ResourceConflictException e) {
+ problems.put(cd.getId(), e.getMessage());
+ } catch (OrmException e) {
+ String msg = "Error checking submit rules for change";
+ log.warn(msg + " " + cd.getId(), e);
+ problems.put(cd.getId(), msg);
+ }
}
}
@@ -371,10 +444,12 @@
logDebug("Beginning integration of {}", change);
try {
ChangeSet cs = mergeSuperSet.completeChangeSet(db, change);
+ reloadChanges(cs);
logDebug("Calculated to merge {}", cs);
if (checkSubmitRules) {
logDebug("Checking submit rules and state");
checkSubmitRulesAndState(cs);
+ failFast(cs); // Done checks that don't involve opening repo.
}
try {
integrateIntoHistory(cs, caller);
@@ -388,65 +463,78 @@
}
}
+ private static void reloadChanges(ChangeSet cs) throws OrmException {
+ // Reload changes in case index was stale.
+ for (ChangeData cd : cs.changes()) {
+ cd.reloadChange();
+ }
+ }
+
+ private void failFast(ChangeSet cs) throws ResourceConflictException {
+ if (problems.isEmpty()) {
+ return;
+ }
+ String msg = "Failed to submit " + cs.size() + " change"
+ + (cs.size() > 1 ? "s" : "") + " due to the following problems:\n";
+ List<String> ps = new ArrayList<>(problems.keySet().size());
+ for (Change.Id id : problems.keySet()) {
+ ps.add("Change " + id + ": " + Joiner.on("; ").join(problems.get(id)));
+ }
+ throw new ResourceConflictException(msg + Joiner.on('\n').join(ps));
+ }
+
private void integrateIntoHistory(ChangeSet cs, IdentifiedUser caller)
throws IntegrationException, NoSuchChangeException,
ResourceConflictException {
logDebug("Beginning merge attempt on {}", cs);
- Map<Branch.NameKey, ListMultimap<SubmitType, ChangeData>> toSubmit =
- new HashMap<>();
+ Map<Branch.NameKey, BranchBatch> toSubmit = new HashMap<>();
logDebug("Perform the merges");
try {
Multimap<Project.NameKey, Branch.NameKey> br = cs.branchesByProject();
Multimap<Branch.NameKey, ChangeData> cbb = cs.changesByBranch();
- for (Project.NameKey project : br.keySet()) {
- openRepository(project);
- for (Branch.NameKey branch : br.get(project)) {
- setDestProject(branch);
-
- ListMultimap<SubmitType, ChangeData> submitting =
- validateChangeList(cbb.get(branch));
- toSubmit.put(branch, submitting);
-
- Set<SubmitType> submitTypes = new HashSet<>(submitting.keySet());
- for (SubmitType submitType : submitTypes) {
- SubmitStrategy strategy = createStrategy(branch, submitType,
- getBranchTip(branch), caller);
-
- MergeTip mergeTip = preMerge(strategy, submitting.get(submitType),
- getBranchTip(branch));
- mergeTips.put(branch, mergeTip);
- updateChangeStatus(submitting.get(submitType), branch,
- true, caller);
- }
- inserter.flush();
- }
- closeRepository();
+ for (Branch.NameKey branch : cbb.keySet()) {
+ OpenRepo or = openRepo(branch.getParentKey());
+ toSubmit.put(branch, validateChangeList(or, cbb.get(branch)));
}
+ failFast(cs); // Done checks that don't involve running submit strategies.
+
+ for (Branch.NameKey branch : cbb.keySet()) {
+ OpenRepo or = openRepo(branch.getParentKey());
+ OpenBranch ob = or.getBranch(branch);
+ BranchBatch submitting = toSubmit.get(branch);
+ SubmitStrategy strategy = createStrategy(or, branch,
+ submitting.submitType(), ob.oldTip, caller);
+ ob.mergeTip = preMerge(strategy, submitting.changes(), ob.oldTip);
+ }
+ checkMergeStrategyResults(cs, toSubmit.values());
+ for (Project.NameKey project : br.keySet()) {
+ openRepo(project).ins.flush();
+ }
+
+ Set<Branch.NameKey> done =
+ Sets.newHashSetWithExpectedSize(cbb.keySet().size());
logDebug("Write out the new branch tips");
SubmoduleOp subOp = subOpProvider.get();
for (Project.NameKey project : br.keySet()) {
- openRepository(project);
+ OpenRepo or = openRepo(project);
for (Branch.NameKey branch : br.get(project)) {
- RefUpdate update = updateBranch(branch, caller);
- pendingRefUpdates.remove(branch);
+ OpenBranch ob = or.getBranch(branch);
+ boolean updated = updateBranch(or, branch, caller);
- setDestProject(branch);
- ListMultimap<SubmitType, ChangeData> submitting = toSubmit.get(branch);
- for (SubmitType submitType : submitting.keySet()) {
- updateChangeStatus(submitting.get(submitType), branch,
- false, caller);
- updateSubmoduleSubscriptions(subOp, branch, getBranchTip(branch));
+ BranchBatch submitting = toSubmit.get(branch);
+ updateChangeStatus(ob, submitting.changes(), caller);
+ updateSubmoduleSubscriptions(ob, subOp);
+ if (updated) {
+ fireRefUpdated(ob);
}
- if (update != null) {
- fireRefUpdated(branch, update);
- }
+ done.add(branch);
}
- closeRepository();
}
updateSuperProjects(subOp, br.values());
- checkState(pendingRefUpdates.isEmpty(), "programmer error: "
- + "pending ref update list not emptied");
+ checkState(done.equals(cbb.keySet()), "programmer error: did not process"
+ + " all branches in input set.\nExpected: %s\nActual: %s",
+ done, cbb.keySet());
} catch (NoSuchProjectException noProject) {
logWarn("Project " + noProject.project() + " no longer exists, "
+ "abandoning open changes");
@@ -455,8 +543,6 @@
throw new IntegrationException("Cannot query the database", e);
} catch (IOException e) {
throw new IntegrationException("Cannot query the database", e);
- } finally {
- closeRepository();
}
}
@@ -478,88 +564,16 @@
return mergeTip;
}
- private SubmitStrategy createStrategy(Branch.NameKey destBranch,
- SubmitType submitType, CodeReviewCommit branchTip, IdentifiedUser caller)
+ private SubmitStrategy createStrategy(OpenRepo or,
+ Branch.NameKey destBranch, SubmitType submitType,
+ CodeReviewCommit branchTip, IdentifiedUser caller)
throws IntegrationException, NoSuchProjectException {
- return submitStrategyFactory.create(submitType, db, repo, rw, inserter,
- canMergeFlag, getAlreadyAccepted(branchTip), destBranch, caller);
+ return submitStrategyFactory.create(submitType, db, or.repo, or.rw, or.ins,
+ or.canMergeFlag, getAlreadyAccepted(or, branchTip), destBranch, caller);
}
- private void openRepository(Project.NameKey name)
- throws IntegrationException, NoSuchProjectException {
- try {
- repo = repoManager.openRepository(name);
- } catch (RepositoryNotFoundException notFound) {
- throw new NoSuchProjectException(name, notFound);
- } catch (IOException err) {
- String m = "Error opening repository \"" + name.get() + '"';
- throw new IntegrationException(m, err);
- }
-
- rw = CodeReviewCommit.newRevWalk(repo);
- rw.sort(RevSort.TOPO);
- rw.sort(RevSort.COMMIT_TIME_DESC, true);
- rw.setRetainBody(false);
- canMergeFlag = rw.newFlag("CAN_MERGE");
-
- inserter = repo.newObjectInserter();
- }
-
- private void closeRepository() {
- if (inserter != null) {
- inserter.close();
- }
- if (rw != null) {
- rw.close();
- }
- if (repo != null) {
- repo.close();
- }
- }
-
- private RefUpdate getPendingRefUpdate(Branch.NameKey destBranch)
- throws IntegrationException {
-
- if (pendingRefUpdates.containsKey(destBranch)) {
- logDebug("Access cached open branch {}: {}", destBranch.get(),
- openBranches.get(destBranch));
- return pendingRefUpdates.get(destBranch);
- }
-
- try {
- RefUpdate branchUpdate = repo.updateRef(destBranch.get());
- CodeReviewCommit branchTip;
- if (branchUpdate.getOldObjectId() != null) {
- branchTip = rw.parseCommit(branchUpdate.getOldObjectId());
- } else if (Objects.equals(repo.getFullBranch(), destBranch.get())) {
- branchTip = null;
- branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
- } else {
- throw new IntegrationException("The destination branch "
- + destBranch.get() + " does not exist anymore.");
- }
-
- logDebug("Opened branch {}: {}", destBranch.get(), branchTip);
- pendingRefUpdates.put(destBranch, branchUpdate);
- openBranches.put(destBranch, branchTip);
- return branchUpdate;
- } catch (IOException e) {
- throw new IntegrationException("Cannot open branch", e);
- }
- }
-
- private CodeReviewCommit getBranchTip(Branch.NameKey destBranch)
- throws IntegrationException {
- if (openBranches.containsKey(destBranch)) {
- return openBranches.get(destBranch);
- } else {
- getPendingRefUpdate(destBranch);
- return openBranches.get(destBranch);
- }
- }
-
- private Set<RevCommit> getAlreadyAccepted(CodeReviewCommit branchTip)
- throws IntegrationException {
+ private Set<RevCommit> getAlreadyAccepted(OpenRepo or,
+ CodeReviewCommit branchTip) throws IntegrationException {
Set<RevCommit> alreadyAccepted = new HashSet<>();
if (branchTip != null) {
@@ -567,9 +581,10 @@
}
try {
- for (Ref r : repo.getRefDatabase().getRefs(Constants.R_HEADS).values()) {
+ for (Ref r : or.repo.getRefDatabase().getRefs(Constants.R_HEADS)
+ .values()) {
try {
- alreadyAccepted.add(rw.parseCommit(r.getObjectId()));
+ alreadyAccepted.add(or.rw.parseCommit(r.getObjectId()));
} catch (IncorrectObjectTypeException iote) {
// Not a commit? Skip over it.
}
@@ -583,41 +598,46 @@
return alreadyAccepted;
}
- private ListMultimap<SubmitType, ChangeData> validateChangeList(
+ @AutoValue
+ static abstract class BranchBatch {
+ abstract SubmitType submitType();
+ abstract List<ChangeData> changes();
+ }
+
+ private void logProblem(Change.Id id, Throwable t) {
+ String msg = "Error reading change";
+ log.error(msg + " " + id, t);
+ problems.put(id, msg);
+ }
+
+ private void logProblem(Change.Id id, String msg) {
+ log.error(msg + " " + id);
+ problems.put(id, msg);
+ }
+
+ private BranchBatch validateChangeList(OpenRepo or,
Collection<ChangeData> submitted) throws IntegrationException {
logDebug("Validating {} changes", submitted.size());
- ListMultimap<SubmitType, ChangeData> toSubmit = ArrayListMultimap.create();
+ List<ChangeData> toSubmit = new ArrayList<>(submitted.size());
+ Multimap<ObjectId, PatchSet.Id> revisions = getRevisions(or, submitted);
- Map<String, Ref> allRefs;
- try {
- allRefs = repo.getRefDatabase().getRefs(ALL);
- } catch (IOException e) {
- throw new IntegrationException(e.getMessage(), e);
- }
-
- Set<ObjectId> tips = new HashSet<>();
- for (Ref r : allRefs.values()) {
- tips.add(r.getObjectId());
- }
-
+ SubmitType submitType = null;
+ ChangeData choseSubmitTypeFrom = null;
for (ChangeData cd : submitted) {
+ Change.Id changeId = cd.getId();
ChangeControl ctl;
Change chg;
try {
ctl = cd.changeControl();
- // Reload change in case index was stale.
- chg = cd.reloadChange();
+ chg = cd.change();
} catch (OrmException e) {
- throw new IntegrationException("Failed to validate changes", e);
- }
- Change.Id changeId = cd.getId();
- if (chg.getStatus() != Change.Status.NEW) {
- logDebug("Change {} is not new: {}", changeId, chg.getStatus());
+ logProblem(changeId, e);
continue;
}
if (chg.currentPatchSetId() == null) {
- logError("Missing current patch set on change " + changeId);
- commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
+ String msg = "Missing current patch set on change";
+ logError(msg + " " + changeId);
+ problems.put(changeId, msg);
continue;
}
@@ -626,12 +646,12 @@
try {
ps = cd.currentPatchSet();
} catch (OrmException e) {
- throw new IntegrationException("Cannot query the database", e);
+ logProblem(changeId, e);
+ continue;
}
if (ps == null || ps.getRevision() == null
|| ps.getRevision().get() == null) {
- logError("Missing patch set or revision on change " + changeId);
- commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
+ logProblem(changeId, "Missing patch set or revision on change");
continue;
}
@@ -639,34 +659,27 @@
ObjectId id;
try {
id = ObjectId.fromString(idstr);
- } catch (IllegalArgumentException iae) {
- logError("Invalid revision on patch set " + ps.getId());
- commits.put(changeId, CodeReviewCommit.noPatchSet(ctl));
+ } catch (IllegalArgumentException e) {
+ logProblem(changeId, e);
continue;
}
- if (!tips.contains(id)) {
- // TODO Technically the proper way to do this test is to use a
- // RevWalk on "$id --not --all" and test for an empty set. But
- // that is way slower than looking for a ref directly pointing
- // at the desired tip. We should always have a ref available.
- //
+ if (!revisions.containsEntry(id, ps.getId())) {
// TODO this is actually an error, the branch is gone but we
// want to merge the issue. We can't safely do that if the
// tip is not reachable.
//
- logError("Revision " + idstr + " of patch set " + ps.getId()
- + " is not contained in any ref");
- commits.put(changeId, CodeReviewCommit.revisionGone(ctl));
+ logProblem(changeId, "Revision " + idstr + " of patch set "
+ + ps.getPatchSetId() + " does not match " + ps.getId().toRefName()
+ + " for change");
continue;
}
CodeReviewCommit commit;
try {
- commit = rw.parseCommit(id);
+ commit = or.rw.parseCommit(id);
} catch (IOException e) {
- logError("Invalid commit " + idstr + " on patch set " + ps.getId(), e);
- commits.put(changeId, CodeReviewCommit.revisionGone(ctl));
+ logProblem(changeId, e);
continue;
}
@@ -678,128 +691,143 @@
MergeValidators mergeValidators = mergeValidatorsFactory.create();
try {
mergeValidators.validatePreMerge(
- repo, commit, destProject, destBranch, ps.getId());
+ or.repo, commit, or.project, destBranch, ps.getId());
} catch (MergeValidationException mve) {
- logDebug("Revision {} of patch set {} failed validation: {}",
- idstr, ps.getId(), mve.getStatus());
- commit.setStatusCode(mve.getStatus());
+ problems.put(changeId, mve.getMessage());
continue;
}
- SubmitType submitType;
- submitType = getSubmitType(commit.getControl(), ps);
+ SubmitType st = getSubmitType(cd);
+ if (st == null) {
+ logProblem(changeId, "No submit type for change");
+ continue;
+ }
if (submitType == null) {
- logError("No submit type for revision " + idstr + " of patch set "
- + ps.getId());
- commit.setStatusCode(CommitMergeStatus.NO_SUBMIT_TYPE);
+ submitType = st;
+ choseSubmitTypeFrom = cd;
+ } else if (st != submitType) {
+ problems.put(changeId, String.format(
+ "Change has submit type %s, but previously chose submit type %s "
+ + "from change %s in the same batch",
+ st, submitType, choseSubmitTypeFrom.getId()));
continue;
}
-
- commit.add(canMergeFlag);
- toSubmit.put(submitType, cd);
+ commit.add(or.canMergeFlag);
+ toSubmit.add(cd);
}
logDebug("Submitting on this run: {}", toSubmit);
- return toSubmit;
+ return new AutoValue_MergeOp_BranchBatch(submitType, toSubmit);
}
- private SubmitType getSubmitType(ChangeControl ctl, PatchSet ps) {
+ private Multimap<ObjectId, PatchSet.Id> getRevisions(OpenRepo or,
+ Collection<ChangeData> cds) throws IntegrationException {
try {
- ChangeData cd = changeDataFactory.create(db, ctl);
- SubmitTypeRecord r = new SubmitRuleEvaluator(cd).setPatchSet(ps)
- .getSubmitType();
- if (r.status != SubmitTypeRecord.Status.OK) {
- logError("Failed to get submit type for " + ctl.getChange().getKey());
- return null;
+ List<String> refNames = new ArrayList<>(cds.size());
+ for (ChangeData cd : cds) {
+ Change c = cd.change();
+ if (c != null) {
+ refNames.add(c.currentPatchSetId().toRefName());
+ }
}
- return r.type;
+ Multimap<ObjectId, PatchSet.Id> revisions =
+ HashMultimap.create(cds.size(), 1);
+ for (Map.Entry<String, Ref> e : or.repo.getRefDatabase().exactRef(
+ refNames.toArray(new String[refNames.size()])).entrySet()) {
+ revisions.put(
+ e.getValue().getObjectId(), PatchSet.Id.fromRef(e.getKey()));
+ }
+ return revisions;
+ } catch (IOException | OrmException e) {
+ throw new IntegrationException("Failed to validate changes", e);
+ }
+ }
+
+ private SubmitType getSubmitType(ChangeData cd) {
+ try {
+ SubmitTypeRecord str = cd.submitTypeRecord();
+ return str.isOk() ? str.type : null;
} catch (OrmException e) {
- logError("Failed to get submit type for " + ctl.getChange().getKey(), e);
+ logError("Failed to get submit type for " + cd.getId(), e);
return null;
}
}
- private RefUpdate updateBranch(Branch.NameKey destBranch,
+ private boolean updateBranch(OpenRepo or, Branch.NameKey destBranch,
IdentifiedUser caller) throws IntegrationException {
- RefUpdate branchUpdate = getPendingRefUpdate(destBranch);
- CodeReviewCommit branchTip = getBranchTip(destBranch);
-
- MergeTip mergeTip = mergeTips.get(destBranch);
-
- CodeReviewCommit currentTip =
- mergeTip != null ? mergeTip.getCurrentTip() : null;
- if (Objects.equals(branchTip, currentTip)) {
+ OpenBranch ob = or.getBranch(destBranch);
+ CodeReviewCommit currentTip = ob.getCurrentTip();
+ if (Objects.equals(ob.oldTip, currentTip)) {
if (currentTip != null) {
logDebug("Branch already at merge tip {}, no update to perform",
currentTip.name());
} else {
logDebug("Both branch and merge tip are nonexistent, no update");
}
- return null;
+ return false;
} else if (currentTip == null) {
logDebug("No merge tip, no update to perform");
- return null;
+ return false;
}
- if (RefNames.REFS_CONFIG.equals(branchUpdate.getName())) {
+ if (RefNames.REFS_CONFIG.equals(ob.update.getName())) {
logDebug("Loading new configuration from {}", RefNames.REFS_CONFIG);
try {
- ProjectConfig cfg =
- new ProjectConfig(destProject.getProject().getNameKey());
- cfg.load(repo, currentTip);
+ ProjectConfig cfg = new ProjectConfig(or.getProjectName());
+ cfg.load(or.repo, currentTip);
} catch (Exception e) {
throw new IntegrationException("Submit would store invalid"
+ " project configuration " + currentTip.name() + " for "
- + destProject.getProject().getName(), e);
+ + or.getProjectName(), e);
}
}
- branchUpdate.setRefLogIdent(
+ ob.update.setRefLogIdent(
identifiedUserFactory.create(caller.getAccountId()).newRefLogIdent());
- branchUpdate.setForceUpdate(false);
- branchUpdate.setNewObjectId(currentTip);
- branchUpdate.setRefLogMessage("merged", true);
+ ob.update.setForceUpdate(false);
+ ob.update.setNewObjectId(currentTip);
+ ob.update.setRefLogMessage("merged", true);
try {
- RefUpdate.Result result = branchUpdate.update(rw);
+ RefUpdate.Result result = ob.update.update(or.rw);
logDebug("Update of {}: {}..{} returned status {}",
- branchUpdate.getName(), branchUpdate.getOldObjectId(),
- branchUpdate.getNewObjectId(), result);
+ ob.update.getName(), ob.update.getOldObjectId(),
+ ob.update.getNewObjectId(), result);
switch (result) {
case NEW:
case FAST_FORWARD:
- if (branchUpdate.getResult() == RefUpdate.Result.FAST_FORWARD) {
+ if (ob.update.getResult() == RefUpdate.Result.FAST_FORWARD) {
tagCache.updateFastForward(destBranch.getParentKey(),
- branchUpdate.getName(),
- branchUpdate.getOldObjectId(),
+ ob.update.getName(),
+ ob.update.getOldObjectId(),
currentTip);
}
- if (RefNames.REFS_CONFIG.equals(branchUpdate.getName())) {
- Project p = destProject.getProject();
+ if (RefNames.REFS_CONFIG.equals(ob.update.getName())) {
+ Project p = or.project.getProject();
projectCache.evict(p);
- destProject = projectCache.get(p.getNameKey());
+ or.project = projectCache.get(p.getNameKey());
repoManager.setProjectDescription(
p.getNameKey(), p.getDescription());
}
- return branchUpdate;
+ return true;
case LOCK_FAILURE:
- throw new IntegrationException("Failed to lock " + branchUpdate.getName());
+ throw new IntegrationException(
+ "Failed to lock " + ob.update.getName());
default:
- throw new IOException(branchUpdate.getResult().name()
- + '\n' + branchUpdate);
+ throw new IOException(
+ ob.update.getResult().name() + '\n' + ob.update);
}
} catch (IOException e) {
- throw new IntegrationException("Cannot update " + branchUpdate.getName(), e);
+ throw new IntegrationException("Cannot update " + ob.update.getName(), e);
}
}
- private void fireRefUpdated(Branch.NameKey destBranch,
- RefUpdate branchUpdate) {
- logDebug("Firing ref updated hooks for {}", branchUpdate.getName());
- gitRefUpdated.fire(destBranch.getParentKey(), branchUpdate);
- hooks.doRefUpdatedHook(destBranch, branchUpdate,
- getAccount(mergeTips.get(destBranch).getCurrentTip()));
+ private void fireRefUpdated(OpenBranch ob) {
+ logDebug("Firing ref updated hooks for {}", ob.name);
+ gitRefUpdated.fire(ob.name.getParentKey(), ob.update);
+ hooks.doRefUpdatedHook(ob.name, ob.update,
+ getAccount(ob.mergeTip.getCurrentTip()));
}
private Account getAccount(CodeReviewCommit codeReviewCommit) {
@@ -820,123 +848,121 @@
return "";
}
- private void updateChangeStatus(List<ChangeData> submitted,
- Branch.NameKey destBranch, boolean dryRun, IdentifiedUser caller)
- throws NoSuchChangeException, IntegrationException, ResourceConflictException,
- OrmException {
- if (!dryRun) {
- logDebug("Updating change status for {} changes", submitted.size());
- } else {
- logDebug("Checking change state for {} changes in a dry run",
- submitted.size());
- }
- MergeTip mergeTip = mergeTips.get(destBranch);
- for (ChangeData cd : submitted) {
- Change c = cd.change();
- CodeReviewCommit commit = commits.get(c.getId());
- CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
- if (s == null) {
- // Shouldn't ever happen, but leave the change alone. We'll pick
- // it up on the next pass.
- //
- logDebug("Submitted change {} did not appear in set of new commits"
- + " produced by merge strategy", c.getId());
- continue;
- }
-
- if (!dryRun) {
- try {
- setApproval(cd, caller);
- } catch (IOException e) {
- throw new OrmException(e);
- }
- }
-
- String txt = s.getMessage();
- logDebug("Status of change {} ({}) on {}: {}", c.getId(), commit.name(),
- c.getDest(), s);
- // If mergeTip is null merge failed and mergeResultRev will not be read.
- ObjectId mergeResultRev =
- mergeTip != null ? mergeTip.getMergeResults().get(commit) : null;
- // The change notes must be forcefully reloaded so that the SUBMIT
- // approval that we added earlier is visible
- commit.notes().reload();
- try {
- ChangeMessage msg;
- switch (s) {
- case CLEAN_MERGE:
- if (!dryRun) {
- setMerged(c, message(c, txt + getByAccountName(commit)),
- mergeResultRev);
- }
- break;
-
- case CLEAN_REBASE:
- case CLEAN_PICK:
- if (!dryRun) {
- setMerged(c, message(c, txt + " as " + commit.name()
- + getByAccountName(commit)), mergeResultRev);
- }
- break;
-
- case ALREADY_MERGED:
- if (!dryRun) {
- setMerged(c, null, mergeResultRev);
- }
- break;
-
- case PATH_CONFLICT:
- case REBASE_MERGE_CONFLICT:
- case MANUAL_RECURSIVE_MERGE:
- case CANNOT_CHERRY_PICK_ROOT:
- case NOT_FAST_FORWARD:
- case INVALID_PROJECT_CONFIGURATION:
- case INVALID_PROJECT_CONFIGURATION_PLUGIN_VALUE_NOT_PERMITTED:
- case INVALID_PROJECT_CONFIGURATION_PLUGIN_VALUE_NOT_EDITABLE:
- case INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND:
- case INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT:
- case SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN:
- setNew(commit.notes(), message(c, txt));
- throw new ResourceConflictException("Cannot merge " + commit.name()
- + "\n" + s.getMessage());
-
- case MISSING_DEPENDENCY:
- logDebug("Change {} is missing dependency", c.getId());
- throw new IntegrationException(
- "Cannot merge " + commit.name() + "\n" + s.getMessage());
-
- case REVISION_GONE:
- logDebug("Commit not found for change {}", c.getId());
- msg = new ChangeMessage(
- new ChangeMessage.Key(
- c.getId(),
- ChangeUtil.messageUUID(db)),
- null,
- TimeUtil.nowTs(),
- c.currentPatchSetId());
- msg.setMessage("Failed to read commit for this patch set");
- setNew(commit.notes(), msg);
- throw new IntegrationException(msg.getMessage());
-
- default:
- msg = message(c, "Unspecified merge failure: " + s.name());
- setNew(commit.notes(), msg);
- throw new IntegrationException(msg.getMessage());
- }
- } catch (OrmException | IOException err) {
- logWarn("Error updating change status for " + c.getId(), err);
- }
- }
+ private Iterable<ChangeData> flattenBatches(Collection<BranchBatch> batches) {
+ return FluentIterable.from(batches)
+ .transformAndConcat(new Function<BranchBatch, List<ChangeData>>() {
+ @Override
+ public List<ChangeData> apply(BranchBatch batch) {
+ return batch.changes();
+ }
+ });
}
- private void updateSubmoduleSubscriptions(SubmoduleOp subOp,
- Branch.NameKey destBranch, CodeReviewCommit branchTip) {
- MergeTip mergeTip = mergeTips.get(destBranch);
+ private void checkMergeStrategyResults(ChangeSet cs,
+ Collection<BranchBatch> batches) throws ResourceConflictException {
+ for (ChangeData cd : flattenBatches(batches)) {
+ Change.Id id = cd.getId();
+ CodeReviewCommit commit = commits.get(id);
+ CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
+ if (s == null) {
+ problems.put(id,
+ "internal error: change not processed by merge strategy");
+ continue;
+ }
+ switch (s) {
+ case CLEAN_MERGE:
+ case CLEAN_REBASE:
+ case CLEAN_PICK:
+ case ALREADY_MERGED:
+ break; // Merge strategy accepted this change.
+
+ case PATH_CONFLICT:
+ case REBASE_MERGE_CONFLICT:
+ case MANUAL_RECURSIVE_MERGE:
+ case CANNOT_CHERRY_PICK_ROOT:
+ case NOT_FAST_FORWARD:
+ // TODO(dborowitz): Reformat these messages to be more appropriate for
+ // short problem descriptions.
+ problems.put(id,
+ CharMatcher.is('\n').collapseFrom(s.getMessage(), ' '));
+ break;
+
+ case MISSING_DEPENDENCY:
+ problems.put(id, "depends on change that was not submitted");
+ break;
+
+ default:
+ problems.put(id, "unspecified merge failure: " + s);
+ break;
+ }
+ }
+ failFast(cs);
+ }
+
+ private void updateChangeStatus(OpenBranch ob, List<ChangeData> submitted,
+ IdentifiedUser caller) throws ResourceConflictException {
+ List<Change.Id> problemChanges = new ArrayList<>(submitted.size());
+ logDebug("Updating change status for {} changes", submitted.size());
+
+ for (ChangeData cd : submitted) {
+ Change.Id id = cd.getId();
+ try {
+ Change c = cd.change();
+ CodeReviewCommit commit = commits.get(id);
+ CommitMergeStatus s = commit != null ? commit.getStatusCode() : null;
+ logDebug("Status of change {} ({}) on {}: {}", id, commit.name(),
+ c.getDest(), s);
+ checkState(s != null,
+ "status not set for change %s; expected to previously fail fast",
+ id);
+ setApproval(cd, caller);
+
+ ObjectId mergeResultRev = ob.mergeTip != null
+ ? ob.mergeTip.getMergeResults().get(commit) : null;
+ String txt = s.getMessage();
+
+ // The change notes must be forcefully reloaded so that the SUBMIT
+ // approval that we added earlier is visible
+ commit.notes().reload();
+ if (s == CommitMergeStatus.CLEAN_MERGE) {
+ setMerged(c, message(c, txt + getByAccountName(commit)),
+ mergeResultRev);
+ } else if (s == CommitMergeStatus.CLEAN_REBASE
+ || s == CommitMergeStatus.CLEAN_PICK) {
+ setMerged(c, message(c, txt + " as " + commit.name()
+ + getByAccountName(commit)), mergeResultRev);
+ } else if (s == CommitMergeStatus.ALREADY_MERGED) {
+ setMerged(c, null, mergeResultRev);
+ } else {
+ throw new IllegalStateException("unexpected status " + s +
+ " for change " + c.getId() + "; expected to previously fail fast");
+ }
+ } catch (OrmException | IOException err) {
+ logWarn("Error updating change status for " + id, err);
+ problemChanges.add(id);
+ }
+ }
+
+ if (problemChanges.isEmpty()) {
+ return;
+ }
+ StringBuilder msg = new StringBuilder("Error updating status of change");
+ if (problemChanges.size() == 1) {
+ msg.append(' ').append(problemChanges.iterator().next());
+ } else {
+ msg.append('s').append(Joiner.on(", ").join(problemChanges));
+ }
+ throw new ResourceConflictException(msg.toString());
+ }
+
+ private void updateSubmoduleSubscriptions(OpenBranch ob, SubmoduleOp subOp) {
+ CodeReviewCommit branchTip = ob.oldTip;
+ MergeTip mergeTip = ob.mergeTip;
if (mergeTip != null
&& (branchTip == null || branchTip != mergeTip.getCurrentTip())) {
- logDebug("Updating submodule subscriptions for branch {}", destBranch);
+ logDebug("Updating submodule subscriptions for branch {}", ob.name);
try {
- subOp.updateSubmoduleSubscriptions(db, destBranch);
+ subOp.updateSubmoduleSubscriptions(db, ob.name);
} catch (SubmoduleException e) {
logError("The submodule subscriptions were not updated according"
+ "to the .gitmodules files", e);
@@ -1053,9 +1079,9 @@
logDebug("Add approval for " + cd + " from user " + user);
ChangeUpdate update = updateFactory.create(control, timestamp);
update.putReviewer(user.getAccountId(), REVIEWER);
- List<SubmitRecord> record = records.get(cd.change().getId());
- if (record != null) {
- update.merge(record);
+ Optional<SubmitRecord> okRecord = findOkRecord(cd.getSubmitRecords());
+ if (okRecord.isPresent()) {
+ update.merge(ImmutableList.of(okRecord.get()));
}
db.changes().beginTransaction(cd.change().getId());
try {
@@ -1170,69 +1196,6 @@
}
}
- private ChangeControl changeControl(Change c) throws NoSuchChangeException {
- return changeControlFactory.controlFor(
- c, identifiedUserFactory.create(c.getOwner()));
- }
-
- private void setNew(ChangeNotes notes, final ChangeMessage msg)
- throws NoSuchChangeException, IOException {
- Change c = notes.getChange();
-
- Change change = null;
- ChangeUpdate update = null;
- try {
- db.changes().beginTransaction(c.getId());
- try {
- change = db.changes().atomicUpdate(
- c.getId(),
- new AtomicUpdate<Change>() {
- @Override
- public Change update(Change c) {
- if (c.getStatus().isOpen()) {
- c.setStatus(Change.Status.NEW);
- ChangeUtil.updated(c);
- }
- return c;
- }
- });
- ChangeControl control = changeControl(change);
-
- //TODO(yyonas): atomic change is not propagated.
- update = updateFactory.create(control, c.getLastUpdatedOn());
- if (msg != null) {
- cmUtil.addChangeMessage(db, update, msg);
- }
- db.commit();
- } finally {
- db.rollback();
- }
- } catch (OrmException err) {
- logWarn("Cannot record merge failure message", err);
- }
- if (update != null) {
- update.commit();
- }
- indexer.index(db, change);
-
- PatchSetApproval submitter = null;
- try {
- submitter = approvalsUtil.getSubmitter(
- db, notes, notes.getChange().currentPatchSetId());
- } catch (Exception e) {
- logError("Cannot get submitter for change " + notes.getChangeId(), e);
- }
- if (submitter != null) {
- try {
- hooks.doMergeFailedHook(c,
- accountCache.get(submitter.getAccountId()).getAccount(),
- db.patchSets().get(c.currentPatchSetId()), msg.getMessage(), db);
- } catch (OrmException ex) {
- logError("Cannot run hook for merge failed " + c.getId(), ex);
- }
- }
- }
-
private void abandonAllOpenChanges(Project.NameKey destProject)
throws NoSuchChangeException {
try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
index aa55751..197b8c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.git;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.strategy.CommitMergeStatus;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevCommitList;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
index c9a5c3e..0c9ddee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -25,7 +25,6 @@
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.Submit;
import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
@@ -104,11 +103,12 @@
for (Change.Id cId : pc.get(project)) {
ChangeData cd = changeDataFactory.create(db, cId);
- SubmitTypeRecord r = new SubmitRuleEvaluator(cd).getSubmitType();
- if (r.status != SubmitTypeRecord.Status.OK) {
- logErrorAndThrow("Failed to get submit type for " + cd.getId());
+ SubmitTypeRecord str = cd.submitTypeRecord();
+ if (!str.isOk()) {
+ logErrorAndThrow("Failed to get submit type for " + cd.getId()
+ + ": " + str.errorMessage);
}
- if (r.type == SubmitType.CHERRY_PICK) {
+ if (str.type == SubmitType.CHERRY_PICK) {
ret.add(cd);
continue;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 436147c..fdb77b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -36,6 +36,7 @@
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.strategy.CommitMergeStatus;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ProjectState;
import com.google.gwtorm.server.OrmException;
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 840b167..540d479 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
@@ -34,7 +34,7 @@
import java.io.IOException;
/** Helps with the updating of a {@link VersionedMetaData}. */
-public class MetaDataUpdate {
+public class MetaDataUpdate implements AutoCloseable {
public static class User {
private final InternalFactory factory;
private final GitRepositoryManager mgr;
@@ -89,6 +89,33 @@
* multiple commits to a single metadata ref, see
* {@link VersionedMetaData#openUpdate(MetaDataUpdate)}.
*
+ * Important: Create a new MetaDataUpdate instance for each update:
+ * <pre>
+ * <code>
+ * try (Repository repo = repoMgr.openRepository(allUsersName);
+ * RevWalk rw = new RevWalk(repo) {
+ * BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
+ * // WRONG: create the MetaDataUpdate instance here and reuse it for
+ * // all updates in the loop
+ * for{@code (Map.Entry<Account.Id, DiffPreferencesInfo> e : diffPrefsFromDb)} {
+ * // CORRECT: create a new MetaDataUpdate instance for each update
+ * try (MetaDataUpdate md =
+ * metaDataUpdateFactory.create(allUsersName, batchUpdate)) {
+ * md.setMessage("Import diff preferences from reviewdb\n");
+ * VersionedAccountPreferences vPrefs =
+ * VersionedAccountPreferences.forUser(e.getKey());
+ * storeSection(vPrefs.getConfig(), UserConfigSections.DIFF, null,
+ * e.getValue(), DiffPreferencesInfo.defaults());
+ * vPrefs.commit(md);
+ * } catch (ConfigInvalidException e) {
+ * // TODO handle exception
+ * }
+ * }
+ * batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
+ * }
+ * </code>
+ * </pre>
+ *
* @param name project name.
* @param repository GIT respository
* @param user user for the update.
@@ -191,6 +218,7 @@
}
/** Close the cached Repository handle. */
+ @Override
public void close() {
getRepository().close();
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReadOnlyRepository.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReadOnlyRepository.java
new file mode 100644
index 0000000..963627d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReadOnlyRepository.java
@@ -0,0 +1,170 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.eclipse.jgit.lib.BaseRepositoryBuilder;
+import org.eclipse.jgit.lib.ObjectDatabase;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.eclipse.jgit.lib.RefRename;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.ReflogReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.StoredConfig;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+class ReadOnlyRepository extends Repository {
+ private static final String MSG =
+ "Cannot modify a " + ReadOnlyRepository.class.getSimpleName();
+
+ private static BaseRepositoryBuilder<?, ?> builder(Repository r) {
+ checkNotNull(r);
+ return new BaseRepositoryBuilder<>()
+ .setFS(r.getFS())
+ .setGitDir(r.getDirectory())
+ .setWorkTree(r.getWorkTree())
+ .setIndexFile(r.getIndexFile());
+ }
+
+ private final Repository delegate;
+ private final RefDb refdb;
+ private final ObjDb objdb;
+
+ ReadOnlyRepository(Repository delegate) {
+ super(builder(delegate));
+ this.delegate = delegate;
+ this.refdb = new RefDb(delegate.getRefDatabase());
+ this.objdb = new ObjDb(delegate.getObjectDatabase());
+ }
+
+ @Override
+ public void create(boolean bare) throws IOException {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ObjectDatabase getObjectDatabase() {
+ return objdb;
+ }
+
+ @Override
+ public RefDatabase getRefDatabase() {
+ return refdb;
+ }
+
+ @Override
+ public StoredConfig getConfig() {
+ return delegate.getConfig();
+ }
+
+ @Override
+ public void scanForRepoChanges() throws IOException {
+ delegate.scanForRepoChanges();
+ }
+
+ @Override
+ public void notifyIndexChanged() {
+ delegate.notifyIndexChanged();
+ }
+
+ @Override
+ public ReflogReader getReflogReader(String refName) throws IOException {
+ return delegate.getReflogReader(refName);
+ }
+
+ private static class RefDb extends RefDatabase {
+ private final RefDatabase delegate;
+
+ private RefDb(RefDatabase delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public void create() throws IOException {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+
+ @Override
+ public boolean isNameConflicting(String name) throws IOException {
+ return delegate.isNameConflicting(name);
+ }
+
+ @Override
+ public RefUpdate newUpdate(String name, boolean detach) throws IOException {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public RefRename newRename(String fromName, String toName)
+ throws IOException {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public Ref getRef(String name) throws IOException {
+ return delegate.getRef(name);
+ }
+
+ @Override
+ public Map<String, Ref> getRefs(String prefix) throws IOException {
+ return delegate.getRefs(prefix);
+ }
+
+ @Override
+ public List<Ref> getAdditionalRefs() throws IOException {
+ return delegate.getAdditionalRefs();
+ }
+
+ @Override
+ public Ref peel(Ref ref) throws IOException {
+ return delegate.peel(ref);
+ }
+ }
+
+ private static class ObjDb extends ObjectDatabase {
+ private final ObjectDatabase delegate;
+
+ private ObjDb(ObjectDatabase delegate) {
+ this.delegate = checkNotNull(delegate);
+ }
+
+ @Override
+ public ObjectInserter newInserter() {
+ throw new UnsupportedOperationException(MSG);
+ }
+
+ @Override
+ public ObjectReader newReader() {
+ return delegate.newReader();
+ }
+
+ @Override
+ public void close() {
+ delegate.close();
+ }
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
index 3ee2a9b..7f556a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.git;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.strategy.CommitMergeStatus;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;
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 3931652..aec8830 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
@@ -179,6 +179,7 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -270,6 +271,9 @@
public RestApiException apply(Exception input) {
if (input instanceof RestApiException) {
return (RestApiException) input;
+ } else if ((input instanceof ExecutionException)
+ && (input.getCause() instanceof RestApiException)) {
+ return (RestApiException) input.getCause();
}
return new RestApiException("Error inserting change/patchset", input);
}
@@ -844,6 +848,9 @@
f.checkedGet();
}
magicBranch.cmd.setResult(OK);
+ } catch (ResourceConflictException e) {
+ addMessage(e.getMessage());
+ reject(magicBranch.cmd, "conflict");
} catch (RestApiException err) {
log.error("Can't insert change/patchset for " + project.getName()
+ ". " + err.getMessage(), err);
@@ -1716,9 +1723,9 @@
private class CreateRequest {
final RevCommit commit;
- final Change change;
final ReceiveCommand cmd;
final ChangeInserter ins;
+ Change change;
boolean created;
Collection<String> groups;
@@ -1755,9 +1762,7 @@
insertChange(threadLocalDb);
}
}
- synchronized (newProgress) {
- newProgress.update(1);
- }
+ synchronizedIncrement(newProgress);
return null;
}
}));
@@ -1810,6 +1815,7 @@
bu.execute();
}
created = true;
+ change = ins.getChange();
if (magicBranch != null && magicBranch.submit) {
submit(projectControl.controlFor(change), ps);
@@ -1821,8 +1827,8 @@
throws OrmException, ResourceConflictException {
Submit submit = submitProvider.get();
RevisionResource rsrc = new RevisionResource(changes.parse(changeCtl), ps);
- try {
- mergeOpProvider.get().merge(db, rsrc.getChange(),
+ try (MergeOp op = mergeOpProvider.get()) {
+ op.merge(db, rsrc.getChange(),
changeCtl.getUser().asIdentifiedUser(), false);
} catch (NoSuchChangeException e) {
throw new OrmException(e);
@@ -2174,9 +2180,7 @@
}
}
} finally {
- synchronized (replaceProgress) {
- replaceProgress.update(1);
- }
+ synchronizedIncrement(replaceProgress);
}
}
}));
@@ -2846,4 +2850,10 @@
private static boolean isConfig(final ReceiveCommand cmd) {
return cmd.getRefName().equals(RefNames.REFS_CONFIG);
}
+
+ private static void synchronizedIncrement(Task p) {
+ synchronized (p) {
+ p.update(1);
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
index 06314db..00c9a7c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
@@ -83,13 +83,8 @@
continue;
}
- try {
- MetaDataUpdate md = metaDataUpdateFactory.create(projectName);
- try {
- rename(md);
- } finally {
- md.close();
- }
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) {
+ rename(md);
} catch (RepositoryNotFoundException noProject) {
continue;
} catch (ConfigInvalidException | IOException err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index 2714015..bb19d0d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -28,7 +28,6 @@
import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
import com.google.gerrit.server.git.BatchUpdate.RepoContext;
import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.CommitMergeStatus;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.IntegrationException;
import com.google.gerrit.server.git.MergeIdenticalTreeException;
@@ -61,7 +60,7 @@
}
@Override
- protected MergeTip _run(CodeReviewCommit branchTip,
+ public MergeTip run(CodeReviewCommit branchTip,
Collection<CodeReviewCommit> toMerge) throws IntegrationException {
MergeTip mergeTip = new MergeTip(branchTip, toMerge);
List<CodeReviewCommit> sorted = CodeReviewCommit.ORDER.sortedCopy(toMerge);
@@ -249,8 +248,8 @@
return newCommits;
}
- @Override
- public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
+ static boolean dryRun(SubmitDryRun.Arguments args,
+ CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
throws IntegrationException {
return args.mergeUtil.canCherryPick(args.mergeSorter, args.repo,
mergeTip, args.rw, toMerge);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CommitMergeStatus.java
similarity index 60%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CommitMergeStatus.java
index 66417fb..cbce1bb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CommitMergeStatus.java
@@ -12,100 +12,55 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.git.strategy;
+/**
+ * Status codes set on {@link com.google.gerrit.server.git.CodeReviewCommit}s by
+ * {@link SubmitStrategy} implementations.
+ */
public enum CommitMergeStatus {
- /** */
CLEAN_MERGE("Change has been successfully merged"),
- /** */
CLEAN_PICK("Change has been successfully cherry-picked"),
- /** */
CLEAN_REBASE("Change has been successfully rebased"),
- /** */
ALREADY_MERGED(""),
- /** */
PATH_CONFLICT("Change could not be merged due to a path conflict.\n"
+ "\n"
+ "Please rebase the change locally and upload the rebased commit for review."),
- /** */
REBASE_MERGE_CONFLICT(
"Change could not be merged due to a conflict.\n"
+ "\n"
+ "Please rebase the change locally and upload the rebased commit for review."),
- /** */
MISSING_DEPENDENCY(""),
- /** */
- NO_PATCH_SET(""),
-
- /** */
- REVISION_GONE(""),
-
- /** */
- NO_SUBMIT_TYPE(""),
-
- /** */
MANUAL_RECURSIVE_MERGE("The change requires a local merge to resolve.\n"
+ "\n"
+ "Please merge (or rebase) the change locally and upload the resolution for review."),
- /** */
CANNOT_CHERRY_PICK_ROOT("Cannot cherry-pick an initial commit onto an existing branch.\n"
+ "\n"
+ "Please merge the change locally and upload the merge commit for review."),
- /** */
CANNOT_REBASE_ROOT("Cannot rebase an initial commit onto an existing branch.\n"
+ "\n"
+ "Please merge the change locally and upload the merge commit for review."),
- /** */
NOT_FAST_FORWARD("Project policy requires all submissions to be a fast-forward.\n"
+ "\n"
- + "Please rebase the change locally and upload again for review."),
-
- /** */
- INVALID_PROJECT_CONFIGURATION("Change contains an invalid project configuration."),
-
- /** */
- INVALID_PROJECT_CONFIGURATION_PLUGIN_VALUE_NOT_PERMITTED(
- "Change contains an invalid project configuration:\n"
- + "One of the plugin configuration parameters has a value that is not permitted."),
-
- /** */
- INVALID_PROJECT_CONFIGURATION_PLUGIN_VALUE_NOT_EDITABLE(
- "Change contains an invalid project configuration:\n"
- + "One of the plugin configuration parameters is not editable."),
-
- /** */
- INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND(
- "Change contains an invalid project configuration:\n"
- + "Parent project does not exist."),
-
- /** */
- INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT(
- "Change contains an invalid project configuration:\n"
- + "The root project cannot have a parent."),
-
- /** */
- SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN(
- "Change contains a project configuration that changes the parent project.\n"
- + "The change must be submitted by a Gerrit administrator.");
-
+ + "Please rebase the change locally and upload again for review.");
private String message;
- CommitMergeStatus(String message){
+ private CommitMergeStatus(String message) {
this.message = message;
}
- public String getMessage(){
+ public String getMessage() {
return message;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
index 67162ea..0da24c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
@@ -15,7 +15,6 @@
package com.google.gerrit.server.git.strategy;
import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.CommitMergeStatus;
import com.google.gerrit.server.git.IntegrationException;
import com.google.gerrit.server.git.MergeTip;
@@ -28,7 +27,7 @@
}
@Override
- protected MergeTip _run(final CodeReviewCommit branchTip,
+ public MergeTip run(final CodeReviewCommit branchTip,
final Collection<CodeReviewCommit> toMerge) throws IntegrationException {
MergeTip mergeTip = new MergeTip(branchTip, toMerge);
List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(
@@ -48,9 +47,9 @@
return mergeTip;
}
- @Override
- public boolean dryRun(CodeReviewCommit mergeTip,
- CodeReviewCommit toMerge) throws IntegrationException {
+ static boolean dryRun(SubmitDryRun.Arguments args,
+ CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
+ throws IntegrationException {
return args.mergeUtil.canFastForward(args.mergeSorter, mergeTip, args.rw,
toMerge);
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
index e7ad79c..4a91b93 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
@@ -29,7 +29,7 @@
}
@Override
- protected MergeTip _run(CodeReviewCommit branchTip,
+ public MergeTip run(CodeReviewCommit branchTip,
Collection<CodeReviewCommit> toMerge) throws IntegrationException {
List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
MergeTip mergeTip;
@@ -59,8 +59,8 @@
return mergeTip;
}
- @Override
- public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
+ static boolean dryRun(SubmitDryRun.Arguments args,
+ CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
throws IntegrationException {
return args.mergeUtil.canMerge(args.mergeSorter, args.repo, mergeTip,
toMerge);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
index 6894b57..64e5be1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
@@ -29,7 +29,7 @@
}
@Override
- protected MergeTip _run(CodeReviewCommit branchTip,
+ public MergeTip run(CodeReviewCommit branchTip,
Collection<CodeReviewCommit> toMerge) throws IntegrationException {
List<CodeReviewCommit> sorted =
args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
@@ -64,8 +64,8 @@
return mergeTip;
}
- @Override
- public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
+ static boolean dryRun(SubmitDryRun.Arguments args,
+ CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
throws IntegrationException {
return args.mergeUtil.canFastForward(
args.mergeSorter, mergeTip, args.rw, toMerge)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
index 9e5fcd2..6acbab9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
@@ -23,14 +23,17 @@
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
+import com.google.gerrit.server.git.BatchUpdate.Context;
+import com.google.gerrit.server.git.BatchUpdate.RepoContext;
import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.CommitMergeStatus;
import com.google.gerrit.server.git.IntegrationException;
import com.google.gerrit.server.git.MergeTip;
import com.google.gerrit.server.git.RebaseSorter;
import com.google.gerrit.server.git.UpdateException;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gwtorm.server.OrmException;
@@ -59,95 +62,203 @@
}
@Override
- protected MergeTip _run(final CodeReviewCommit branchTip,
+ public MergeTip run(final CodeReviewCommit branchTip,
final Collection<CodeReviewCommit> toMerge) throws IntegrationException {
MergeTip mergeTip = new MergeTip(branchTip, toMerge);
List<CodeReviewCommit> sorted = sort(toMerge);
- while (!sorted.isEmpty()) {
- CodeReviewCommit n = sorted.remove(0);
- if (mergeTip.getCurrentTip() == null) {
- // The branch is unborn. Take a fast-forward resolution to
- // create the branch.
- //
- n.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
- mergeTip.moveTipTo(n, n);
+ boolean first = true;
- } else if (n.getParentCount() == 0) {
- // Refuse to merge a root commit into an existing branch,
- // we cannot obtain a delta for the rebase to apply.
- //
- n.setStatusCode(CommitMergeStatus.CANNOT_REBASE_ROOT);
-
- } else if (n.getParentCount() == 1) {
- if (args.mergeUtil.canFastForward(args.mergeSorter,
- mergeTip.getCurrentTip(), args.rw, n)) {
- n.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
- mergeTip.moveTipTo(n, n);
-
+ try (BatchUpdate u = args.newBatchUpdate(TimeUtil.nowTs())) {
+ while (!sorted.isEmpty()) {
+ CodeReviewCommit n = sorted.remove(0);
+ Change.Id cid = n.change().getId();
+ if (first && branchTip == null) {
+ u.addOp(cid, new RebaseUnbornRootOp(mergeTip, n));
+ } else if (n.getParentCount() == 0) {
+ u.addOp(cid, new RebaseRootOp(n));
+ } else if (n.getParentCount() == 1) {
+ u.addOp(cid, new RebaseOneOp(mergeTip, n));
} else {
- try {
- PatchSet newPatchSet = rebase(n, mergeTip);
- List<PatchSetApproval> approvals = Lists.newArrayList();
- for (PatchSetApproval a : args.approvalsUtil.byPatchSet(args.db,
- n.getControl(), n.getPatchsetId())) {
- approvals.add(new PatchSetApproval(newPatchSet.getId(), a));
- }
- // rebaseChange.rebase() may already have copied some approvals,
- // use upsert, not insert, to avoid constraint violation on database
- args.db.patchSetApprovals().upsert(approvals);
- CodeReviewCommit newTip = args.rw.parseCommit(
- ObjectId.fromString(newPatchSet.getRevision().get()));
- mergeTip.moveTipTo(newTip, newTip);
- n.change().setCurrentPatchSet(
- patchSetInfoFactory.get(args.rw, mergeTip.getCurrentTip(),
- newPatchSet.getId()));
- mergeTip.getCurrentTip().copyFrom(n);
- mergeTip.getCurrentTip().setControl(
- args.changeControlFactory.controlFor(n.change(), args.caller));
- mergeTip.getCurrentTip().setPatchsetId(newPatchSet.getId());
- mergeTip.getCurrentTip().setStatusCode(
- CommitMergeStatus.CLEAN_REBASE);
- newCommits.put(newPatchSet.getId().getParentKey(),
- mergeTip.getCurrentTip());
- } catch (MergeConflictException e) {
- n.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
- throw new IntegrationException(
- "Cannot rebase " + n.name() + ": " + e.getMessage(), e);
- } catch (NoSuchChangeException | OrmException | IOException
- | RestApiException | UpdateException e) {
- throw new IntegrationException("Cannot rebase " + n.name(), e);
- }
+ u.addOp(cid, new RebaseMultipleParentsOp(mergeTip, n));
}
-
- } else if (n.getParentCount() > 1) {
- // There are multiple parents, so this is a merge commit. We
- // don't want to rebase the merge as clients can't easily
- // rebase their history with that merge present and replaced
- // by an equivalent merge with a different first parent. So
- // instead behave as though MERGE_IF_NECESSARY was configured.
- //
- try {
- if (args.rw.isMergedInto(mergeTip.getCurrentTip(), n)) {
- mergeTip.moveTipTo(n, n);
- } else {
- PersonIdent myIdent = args.serverIdent.get();
- mergeTip.moveTipTo(
- args.mergeUtil.mergeOneCommit(myIdent, myIdent,
- args.repo, args.rw, args.inserter, args.canMergeFlag,
- args.destBranch, mergeTip.getCurrentTip(), n), n);
- }
- args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
- mergeTip.getCurrentTip(), args.alreadyAccepted);
- } catch (IOException e) {
- throw new IntegrationException("Cannot merge " + n.name(), e);
- }
+ first = false;
}
+ u.execute();
+ } catch (UpdateException e) {
+ if (e.getCause() instanceof IntegrationException) {
+ throw new IntegrationException(e.getCause().getMessage(), e);
+ }
+ throw new IntegrationException(
+ "Cannot rebase onto " + args.destBranch);
+ } catch (RestApiException e) {
+ throw new IntegrationException(
+ "Cannot rebase onto " + args.destBranch);
+ }
+ return mergeTip;
+ }
- args.alreadyAccepted.add(mergeTip.getCurrentTip());
+ private class RebaseUnbornRootOp extends BatchUpdate.Op {
+ private final MergeTip mergeTip;
+ private final CodeReviewCommit toMerge;
+
+ private RebaseUnbornRootOp(MergeTip mergeTip,
+ CodeReviewCommit toMerge) {
+ this.mergeTip = mergeTip;
+ this.toMerge = toMerge;
}
- return mergeTip;
+ @Override
+ public void updateRepo(RepoContext ctx) {
+ // The branch is unborn. Take fast-forward resolution to create the
+ // branch.
+ toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
+ mergeTip.moveTipTo(toMerge, toMerge);
+ acceptMergeTip(mergeTip);
+ }
+ }
+
+ private static class RebaseRootOp extends BatchUpdate.Op {
+ private final CodeReviewCommit toMerge;
+
+ private RebaseRootOp(CodeReviewCommit toMerge) {
+ this.toMerge = toMerge;
+ }
+
+ @Override
+ public void updateRepo(RepoContext ctx) {
+ // Refuse to merge a root commit into an existing branch, we cannot obtain
+ // a delta for the cherry-pick to apply.
+ toMerge.setStatusCode(CommitMergeStatus.CANNOT_REBASE_ROOT);
+ }
+ }
+
+ private class RebaseOneOp extends BatchUpdate.Op {
+ private final MergeTip mergeTip;
+ private final CodeReviewCommit toMerge;
+
+ private RebaseChangeOp rebaseOp;
+
+ private RebaseOneOp(MergeTip mergeTip, CodeReviewCommit toMerge) {
+ this.mergeTip = mergeTip;
+ this.toMerge = toMerge;
+ }
+
+ @Override
+ public void updateRepo(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,
+ mergeTip.getCurrentTip(), args.rw, toMerge)) {
+ toMerge.setStatusCode(CommitMergeStatus.CLEAN_MERGE);
+ mergeTip.moveTipTo(toMerge, toMerge);
+ acceptMergeTip(mergeTip);
+ return;
+ }
+
+ rebaseOp = rebaseFactory.create(
+ toMerge.getControl(),
+ // Racy read of patch set is ok; see comments in RebaseChangeOp.
+ args.db.patchSets().get(toMerge.getPatchsetId()),
+ mergeTip.getCurrentTip().name())
+ .setCommitterIdent(args.serverIdent.get())
+ .setRunHooks(false)
+ .setValidatePolicy(CommitValidators.Policy.NONE);
+ try {
+ rebaseOp.updateRepo(ctx);
+ } catch (MergeConflictException e) {
+ toMerge.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
+ throw new IntegrationException(
+ "Cannot rebase " + toMerge.name() + ": " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void updateChange(ChangeContext ctx) throws NoSuchChangeException,
+ InvalidChangeOperationException, OrmException, IOException {
+ if (rebaseOp == null) {
+ // Took the fast-forward option, nothing to do.
+ return;
+ }
+
+ rebaseOp.updateChange(ctx);
+ PatchSet newPatchSet = rebaseOp.getPatchSet();
+ List<PatchSetApproval> approvals = Lists.newArrayList();
+ for (PatchSetApproval a : args.approvalsUtil.byPatchSet(ctx.getDb(),
+ toMerge.getControl(), toMerge.getPatchsetId())) {
+ approvals.add(new PatchSetApproval(newPatchSet.getId(), a));
+ }
+ // rebaseOp may already have copied some approvals; use upsert, not
+ // insert, to avoid constraint violation on database.
+ args.db.patchSetApprovals().upsert(approvals);
+
+ // TODO(dborowitz): Make RevWalk available via BatchUpdate.
+ CodeReviewCommit newTip = args.rw.parseCommit(
+ ObjectId.fromString(newPatchSet.getRevision().get()));
+ mergeTip.moveTipTo(newTip, newTip);
+ toMerge.change().setCurrentPatchSet(
+ patchSetInfoFactory.get(args.rw, mergeTip.getCurrentTip(),
+ newPatchSet.getId()));
+ mergeTip.getCurrentTip().copyFrom(toMerge);
+ mergeTip.getCurrentTip().setControl(
+ args.changeControlFactory.controlFor(toMerge.change(), args.caller));
+ mergeTip.getCurrentTip().setPatchsetId(newPatchSet.getId());
+ mergeTip.getCurrentTip().setStatusCode(
+ CommitMergeStatus.CLEAN_REBASE);
+ newCommits.put(newPatchSet.getId().getParentKey(),
+ mergeTip.getCurrentTip());
+ acceptMergeTip(mergeTip);
+ }
+
+ @Override
+ public void postUpdate(Context ctx) throws OrmException {
+ if (rebaseOp != null) {
+ rebaseOp.postUpdate(ctx);
+ }
+ }
+ }
+
+ private class RebaseMultipleParentsOp extends BatchUpdate.Op {
+ private final MergeTip mergeTip;
+ private final CodeReviewCommit toMerge;
+
+ private RebaseMultipleParentsOp(MergeTip mergeTip,
+ CodeReviewCommit toMerge) {
+ this.mergeTip = mergeTip;
+ this.toMerge = toMerge;
+ }
+
+ @Override
+ public void updateRepo(RepoContext ctx)
+ throws IntegrationException, IOException {
+ // There are multiple parents, so this is a merge commit. We don't want
+ // to rebase the merge as clients can't easily rebase their history with
+ // that merge present and replaced by an equivalent merge with a different
+ // first parent. So instead behave as though MERGE_IF_NECESSARY was
+ // configured.
+ if (args.rw.isMergedInto(mergeTip.getCurrentTip(), toMerge)) {
+ mergeTip.moveTipTo(toMerge, toMerge);
+ acceptMergeTip(mergeTip);
+ } else {
+ PersonIdent myIdent = args.serverIdent.get();
+ // TODO(dborowitz): Can't use repo from ctx due to canMergeFlag.
+ CodeReviewCommit newTip = args.mergeUtil.mergeOneCommit(
+ myIdent, myIdent, args.repo, args.rw, args.inserter,
+ args.canMergeFlag, args.destBranch, mergeTip.getCurrentTip(),
+ toMerge);
+ mergeTip.moveTipTo(newTip, toMerge);
+ }
+ args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+ mergeTip.getCurrentTip(), args.alreadyAccepted);
+ acceptMergeTip(mergeTip);
+ }
+ }
+
+ private void acceptMergeTip(MergeTip mergeTip) {
+ args.alreadyAccepted.add(mergeTip.getCurrentTip());
}
private List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort)
@@ -162,29 +273,13 @@
}
}
- private PatchSet rebase(CodeReviewCommit n, MergeTip mergeTip)
- throws RestApiException, UpdateException, OrmException {
- RebaseChangeOp op = rebaseFactory.create(
- n.getControl(),
- args.db.patchSets().get(n.getPatchsetId()),
- mergeTip.getCurrentTip().name())
- .setCommitterIdent(args.serverIdent.get())
- .setRunHooks(false)
- .setValidatePolicy(CommitValidators.Policy.NONE);
- try (BatchUpdate bu = args.newBatchUpdate(TimeUtil.nowTs())) {
- bu.addOp(n.change().getId(), op);
- bu.execute();
- }
- return op.getPatchSet();
- }
-
@Override
public Map<Change.Id, CodeReviewCommit> getNewCommits() {
return newCommits;
}
- @Override
- public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
+ static boolean dryRun(SubmitDryRun.Arguments args,
+ CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
throws IntegrationException {
return !args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)
&& args.mergeUtil.canCherryPick(args.mergeSorter, args.repo, mergeTip,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
new file mode 100644
index 0000000..c784379
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
@@ -0,0 +1,146 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.strategy;
+
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.gerrit.extensions.client.SubmitType;
+import com.google.gerrit.reviewdb.client.Branch;
+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.MergeSorter;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Constants;
+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.RevFlag;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/** Dry run of a submit strategy. */
+public class SubmitDryRun {
+ private static final Logger log = LoggerFactory.getLogger(SubmitDryRun.class);
+
+ static class Arguments {
+ final Repository repo;
+ final CodeReviewRevWalk rw;
+ final MergeUtil mergeUtil;
+ final MergeSorter mergeSorter;
+
+ Arguments(Repository repo,
+ CodeReviewRevWalk rw,
+ MergeUtil mergeUtil,
+ MergeSorter mergeSorter) {
+ this.repo = repo;
+ this.rw = rw;
+ this.mergeUtil = mergeUtil;
+ this.mergeSorter = mergeSorter;
+ }
+ }
+
+ public static Iterable<ObjectId> getAlreadyAccepted(Repository repo)
+ throws IOException {
+ return FluentIterable
+ .from(repo.getRefDatabase().getRefs(Constants.R_HEADS).values())
+ .append(repo.getRefDatabase().getRefs(Constants.R_TAGS).values())
+ .transform(new Function<Ref, ObjectId>() {
+ @Override
+ public ObjectId apply(Ref r) {
+ return r.getObjectId();
+ }
+ });
+ }
+
+ public static Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw)
+ throws IOException {
+ Set<RevCommit> accepted = new HashSet<>();
+ addCommits(getAlreadyAccepted(repo), rw, accepted);
+ return accepted;
+ }
+
+ public static void addCommits(Iterable<ObjectId> ids, RevWalk rw,
+ Collection<RevCommit> out) throws IOException {
+ for (ObjectId id : ids) {
+ RevObject obj = rw.parseAny(id);
+ if (obj instanceof RevCommit) {
+ out.add((RevCommit) obj);
+ }
+ }
+ }
+
+ private final ProjectCache projectCache;
+ private final MergeUtil.Factory mergeUtilFactory;
+
+ @Inject
+ SubmitDryRun(ProjectCache projectCache,
+ MergeUtil.Factory mergeUtilFactory) {
+ this.projectCache = projectCache;
+ this.mergeUtilFactory = mergeUtilFactory;
+ }
+
+ public boolean run(SubmitType submitType, Repository repo,
+ CodeReviewRevWalk rw, Branch.NameKey destBranch, ObjectId tip,
+ ObjectId toMerge, Set<RevCommit> alreadyAccepted)
+ throws IntegrationException, NoSuchProjectException, IOException {
+ CodeReviewCommit tipCommit = rw.parseCommit(tip);
+ CodeReviewCommit toMergeCommit = rw.parseCommit(toMerge);
+ RevFlag canMerge = rw.newFlag("CAN_MERGE");
+ toMergeCommit.add(canMerge);
+ Arguments args = new Arguments(repo, rw,
+ mergeUtilFactory.create(getProject(destBranch)),
+ new MergeSorter(rw, alreadyAccepted, canMerge));
+
+ switch (submitType) {
+ case CHERRY_PICK:
+ return CherryPick.dryRun(args, tipCommit, toMergeCommit);
+ case FAST_FORWARD_ONLY:
+ return FastForwardOnly.dryRun(args, tipCommit, toMergeCommit);
+ case MERGE_ALWAYS:
+ return MergeAlways.dryRun(args, tipCommit, toMergeCommit);
+ case MERGE_IF_NECESSARY:
+ return MergeIfNecessary.dryRun(args, tipCommit, toMergeCommit);
+ case REBASE_IF_NECESSARY:
+ return RebaseIfNecessary.dryRun(args, tipCommit, toMergeCommit);
+ default:
+ String errorMsg = "No submit strategy for: " + submitType;
+ log.error(errorMsg);
+ throw new IntegrationException(errorMsg);
+ }
+ }
+
+ private ProjectState getProject(Branch.NameKey branch)
+ throws NoSuchProjectException {
+ ProjectState p = projectCache.get(branch.getParentKey());
+ if (p == null) {
+ throw new NoSuchProjectException(branch.getParentKey());
+ }
+ return p;
+ }
+}
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 c4c7e88..c8dd655 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
@@ -14,7 +14,7 @@
package com.google.gerrit.server.git.strategy;
-import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Preconditions.checkNotNull;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Branch;
@@ -29,7 +29,6 @@
import com.google.gerrit.server.git.MergeSorter;
import com.google.gerrit.server.git.MergeTip;
import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.project.ChangeControl;
import com.google.inject.Provider;
@@ -53,7 +52,6 @@
*/
public abstract class SubmitStrategy {
static class Arguments {
- protected final IdentifiedUser.GenericFactory identifiedUserFactory;
protected final Provider<PersonIdent> serverIdent;
protected final ReviewDb db;
protected final BatchUpdate.Factory batchUpdateFactory;
@@ -67,35 +65,32 @@
protected final Branch.NameKey destBranch;
protected final ApprovalsUtil approvalsUtil;
protected final MergeUtil mergeUtil;
- protected final ChangeIndexer indexer;
protected final MergeSorter mergeSorter;
protected final IdentifiedUser caller;
- Arguments(IdentifiedUser.GenericFactory identifiedUserFactory,
- Provider<PersonIdent> serverIdent, ReviewDb db,
+ Arguments(Provider<PersonIdent> serverIdent, ReviewDb db,
BatchUpdate.Factory batchUpdateFactory,
ChangeControl.GenericFactory changeControlFactory, Repository repo,
CodeReviewRevWalk rw, ObjectInserter inserter, RevFlag canMergeFlag,
Set<RevCommit> alreadyAccepted, Branch.NameKey destBranch,
ApprovalsUtil approvalsUtil, MergeUtil mergeUtil,
- ChangeIndexer indexer, IdentifiedUser caller) {
- this.identifiedUserFactory = identifiedUserFactory;
- this.serverIdent = serverIdent;
- this.db = db;
- this.batchUpdateFactory = batchUpdateFactory;
- this.changeControlFactory = changeControlFactory;
+ IdentifiedUser caller) {
+ this.serverIdent = checkNotNull(serverIdent);
+ this.db = checkNotNull(db);
+ this.batchUpdateFactory = checkNotNull(batchUpdateFactory);
+ this.changeControlFactory = checkNotNull(changeControlFactory);
- this.repo = repo;
- this.rw = rw;
- this.inserter = inserter;
- this.canMergeFlag = canMergeFlag;
- this.alreadyAccepted = alreadyAccepted;
- this.destBranch = destBranch;
- this.approvalsUtil = approvalsUtil;
- this.mergeUtil = mergeUtil;
- this.indexer = indexer;
+ this.repo = checkNotNull(repo);
+ this.rw = checkNotNull(rw);
+ this.inserter = checkNotNull(inserter);
+ this.canMergeFlag = checkNotNull(canMergeFlag);
+ this.alreadyAccepted = checkNotNull(alreadyAccepted);
+ this.destBranch = checkNotNull(destBranch);
+ this.approvalsUtil = checkNotNull(approvalsUtil);
+ this.mergeUtil = checkNotNull(mergeUtil);
+ this.caller = checkNotNull(caller);
+
this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
- this.caller = caller;
}
BatchUpdate newBatchUpdate(Timestamp when) {
@@ -108,7 +103,7 @@
protected final Arguments args;
SubmitStrategy(Arguments args) {
- this.args = args;
+ this.args = checkNotNull(args);
}
/**
@@ -123,32 +118,10 @@
* @return the new merge tip.
* @throws IntegrationException
*/
- public final MergeTip run(final CodeReviewCommit currentTip,
- final Collection<CodeReviewCommit> toMerge) throws IntegrationException {
- checkState(args.caller != null);
- return _run(currentTip, toMerge);
- }
-
- /** @see #run(CodeReviewCommit, Collection) */
- protected abstract MergeTip _run(CodeReviewCommit currentTip,
+ public abstract MergeTip run(CodeReviewCommit currentTip,
Collection<CodeReviewCommit> toMerge) throws IntegrationException;
/**
- * Checks whether the given commit can be merged.
- * <p>
- * Implementations must ensure that invoking this method modifies neither the
- * git repository nor the Gerrit database.
- *
- * @param mergeTip the merge tip.
- * @param toMerge the commit that should be checked.
- * @return {@code true} if the given commit can be merged, otherwise
- * {@code false}
- * @throws IntegrationException
- */
- public abstract boolean dryRun(CodeReviewCommit mergeTip,
- CodeReviewCommit toMerge) throws IntegrationException;
-
- /**
* Returns all commits that have been newly created for the changes that are
* getting merged.
* <p>
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 0c8c2f0..485e265 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
@@ -25,7 +25,6 @@
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.IntegrationException;
import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.index.ChangeIndexer;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchProjectException;
@@ -51,7 +50,6 @@
private static final Logger log = LoggerFactory
.getLogger(SubmitStrategyFactory.class);
- private final IdentifiedUser.GenericFactory identifiedUserFactory;
private final Provider<PersonIdent> myIdent;
private final BatchUpdate.Factory batchUpdateFactory;
private final ChangeControl.GenericFactory changeControlFactory;
@@ -60,21 +58,17 @@
private final ProjectCache projectCache;
private final ApprovalsUtil approvalsUtil;
private final MergeUtil.Factory mergeUtilFactory;
- private final ChangeIndexer indexer;
@Inject
SubmitStrategyFactory(
- final IdentifiedUser.GenericFactory identifiedUserFactory,
@GerritPersonIdent Provider<PersonIdent> myIdent,
- final BatchUpdate.Factory batchUpdateFactory,
- final ChangeControl.GenericFactory changeControlFactory,
- final PatchSetInfoFactory patchSetInfoFactory,
- final RebaseChangeOp.Factory rebaseFactory,
- final ProjectCache projectCache,
- final ApprovalsUtil approvalsUtil,
- final MergeUtil.Factory mergeUtilFactory,
- final ChangeIndexer indexer) {
- this.identifiedUserFactory = identifiedUserFactory;
+ BatchUpdate.Factory batchUpdateFactory,
+ ChangeControl.GenericFactory changeControlFactory,
+ PatchSetInfoFactory patchSetInfoFactory,
+ RebaseChangeOp.Factory rebaseFactory,
+ ProjectCache projectCache,
+ ApprovalsUtil approvalsUtil,
+ MergeUtil.Factory mergeUtilFactory) {
this.myIdent = myIdent;
this.batchUpdateFactory = batchUpdateFactory;
this.changeControlFactory = changeControlFactory;
@@ -83,7 +77,6 @@
this.projectCache = projectCache;
this.approvalsUtil = approvalsUtil;
this.mergeUtilFactory = mergeUtilFactory;
- this.indexer = indexer;
}
public SubmitStrategy create(SubmitType submitType, ReviewDb db,
@@ -93,10 +86,9 @@
throws IntegrationException, NoSuchProjectException {
ProjectState project = getProject(destBranch);
SubmitStrategy.Arguments args = new SubmitStrategy.Arguments(
- identifiedUserFactory, myIdent, db, batchUpdateFactory,
- changeControlFactory, repo, rw, inserter, canMergeFlag, alreadyAccepted,
- destBranch,approvalsUtil, mergeUtilFactory.create(project), indexer,
- caller);
+ myIdent, db, batchUpdateFactory, changeControlFactory,
+ repo, rw, inserter, canMergeFlag, alreadyAccepted,
+ destBranch,approvalsUtil, mergeUtilFactory.create(project), caller);
switch (submitType) {
case CHERRY_PICK:
return new CherryPick(args, patchSetInfoFactory);
@@ -109,7 +101,7 @@
case REBASE_IF_NECESSARY:
return new RebaseIfNecessary(args, patchSetInfoFactory, rebaseFactory);
default:
- final String errorMsg = "No submit strategy for: " + submitType;
+ String errorMsg = "No submit strategy for: " + submitType;
log.error(errorMsg);
throw new IntegrationException(errorMsg);
}
@@ -117,7 +109,7 @@
private ProjectState getProject(Branch.NameKey branch)
throws NoSuchProjectException {
- final ProjectState p = projectCache.get(branch.getParentKey());
+ ProjectState p = projectCache.get(branch.getParentKey());
if (p == null) {
throw new NoSuchProjectException(branch.getParentKey());
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationException.java
index bfad0e3..018ec1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidationException.java
@@ -14,19 +14,18 @@
package com.google.gerrit.server.git.validators;
-import com.google.gerrit.server.git.CommitMergeStatus;
import com.google.gerrit.server.validators.ValidationException;
+/**
+ * Exception that occurs during a validation step before merging changes.
+ * <p>
+ * Used by {@link MergeValidationListener}s provided by plugins. Messages should
+ * be considered human-readable.
+ */
public class MergeValidationException extends ValidationException {
private static final long serialVersionUID = 1L;
- private final CommitMergeStatus status;
- public MergeValidationException(CommitMergeStatus status) {
- super(status.toString());
- this.status = status;
- }
-
- public CommitMergeStatus getStatus() {
- return status;
+ public MergeValidationException(String msg) {
+ super(msg);
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
index 6f70d46..e658537 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -30,7 +30,6 @@
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.config.ProjectConfigEntry;
import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.CommitMergeStatus;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
@@ -75,6 +74,26 @@
public static class ProjectConfigValidator implements
MergeValidationListener {
+ private static final String INVALID_CONFIG =
+ "Change contains an invalid project configuration.";
+ private static final String PARENT_NOT_FOUND =
+ "Change contains an invalid project configuration:\n"
+ + "Parent project does not exist.";
+ private static final String PLUGIN_VALUE_NOT_EDITABLE =
+ "Change contains an invalid project configuration:\n"
+ + "One of the plugin configuration parameters is not editable.";
+ private static final String PLUGIN_VALUE_NOT_PERMITTED =
+ "Change contains an invalid project configuration:\n"
+ + "One of the plugin configuration parameters has a value that is not"
+ + " permitted.";
+ private static final String ROOT_NO_PARENT =
+ "Change contains an invalid project configuration:\n"
+ + "The root project cannot have a parent.";
+ private static final String SET_BY_ADMIN =
+ "Change contains a project configuration that changes the parent"
+ + " project.\n"
+ + "The change must be submitted by a Gerrit administrator.";
+
private final AllProjectsName allProjectsName;
private final ReviewDb db;
private final ProjectCache projectCache;
@@ -119,27 +138,23 @@
if (oldParent == null) {
// update of the 'All-Projects' project
if (newParent != null) {
- throw new MergeValidationException(CommitMergeStatus.
- INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT);
+ throw new MergeValidationException(ROOT_NO_PARENT);
}
} else {
if (!oldParent.equals(newParent)) {
PatchSetApproval psa =
approvalsUtil.getSubmitter(db, commit.notes(), patchSetId);
if (psa == null) {
- throw new MergeValidationException(CommitMergeStatus.
- SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN);
+ throw new MergeValidationException(SET_BY_ADMIN);
}
final IdentifiedUser submitter =
identifiedUserFactory.create(psa.getAccountId());
if (!submitter.getCapabilities().canAdministrateServer()) {
- throw new MergeValidationException(CommitMergeStatus.
- SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN);
+ throw new MergeValidationException(SET_BY_ADMIN);
}
if (projectCache.get(newParent) == null) {
- throw new MergeValidationException(CommitMergeStatus.
- INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND);
+ throw new MergeValidationException(PARENT_NOT_FOUND);
}
}
}
@@ -155,19 +170,16 @@
if ((value == null ? oldValue != null : !value.equals(oldValue)) &&
!configEntry.isEditable(destProject)) {
- throw new MergeValidationException(CommitMergeStatus.
- INVALID_PROJECT_CONFIGURATION_PLUGIN_VALUE_NOT_EDITABLE);
+ throw new MergeValidationException(PLUGIN_VALUE_NOT_EDITABLE);
}
if (ProjectConfigEntry.Type.LIST.equals(configEntry.getType())
&& value != null && !configEntry.getPermittedValues().contains(value)) {
- throw new MergeValidationException(CommitMergeStatus.
- INVALID_PROJECT_CONFIGURATION_PLUGIN_VALUE_NOT_PERMITTED);
+ throw new MergeValidationException(PLUGIN_VALUE_NOT_PERMITTED);
}
}
} catch (ConfigInvalidException | IOException e) {
- throw new MergeValidationException(CommitMergeStatus.
- INVALID_PROJECT_CONFIGURATION);
+ throw new MergeValidationException(INVALID_CONFIG);
}
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
index 4fa5cd3..2c7aff8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeField.java
@@ -65,19 +65,8 @@
* characters.
*/
public class ChangeField {
- @Deprecated
/** Legacy change ID. */
public static final FieldDef<ChangeData, Integer> LEGACY_ID =
- new FieldDef.Single<ChangeData, Integer>("_id",
- FieldType.INTEGER, true) {
- @Override
- public Integer get(ChangeData input, FillArgs args) {
- return input.getId().get();
- }
- };
-
- /** Legacy change ID without underscore prefix. */
- public static final FieldDef<ChangeData, Integer> LEGACY_ID2 =
new FieldDef.Single<ChangeData, Integer>("legacy_id",
FieldType.INTEGER, true) {
@Override
@@ -161,30 +150,6 @@
}
};
- @Deprecated
- /** Topic, a short annotation on the branch. */
- public static final FieldDef<ChangeData, String> LEGACY_TOPIC2 =
- new FieldDef.Single<ChangeData, String>(
- "topic2", FieldType.EXACT, false) {
- @Override
- public String get(ChangeData input, FillArgs args)
- throws OrmException {
- return getTopic(input);
- }
- };
-
- @Deprecated
- /** Topic, a short annotation on the branch. */
- public static final FieldDef<ChangeData, String> LEGACY_TOPIC3 =
- new FieldDef.Single<ChangeData, String>(
- "topic3", FieldType.PREFIX, false) {
- @Override
- public String get(ChangeData input, FillArgs args)
- throws OrmException {
- return getTopic(input);
- }
- };
-
/** Topic, a short annotation on the branch. */
public static final FieldDef<ChangeData, String> EXACT_TOPIC =
new FieldDef.Single<ChangeData, String>(
@@ -405,23 +370,6 @@
}
};
- /** Set true if the change has a non-zero label score. */
- @Deprecated
- public static final FieldDef<ChangeData, String> LEGACY_REVIEWED =
- new FieldDef.Single<ChangeData, String>(
- "reviewed", FieldType.EXACT, false) {
- @Override
- public String get(ChangeData input, FillArgs args)
- throws OrmException {
- for (PatchSetApproval a : input.currentApprovals()) {
- if (a.getValue() != 0) {
- return "1";
- }
- }
- return null;
- }
- };
-
private static Set<String> getPersonParts(PersonIdent person) {
if (person == null) {
return ImmutableSet.of();
@@ -696,6 +644,24 @@
}
};
+
+ /** Users who have draft comments on this change. */
+ public static final FieldDef<ChangeData, Iterable<Integer>> DRAFTBY =
+ new FieldDef.Repeatable<ChangeData, Integer>(
+ ChangeQueryBuilder.FIELD_DRAFTBY, FieldType.INTEGER, false) {
+ @Override
+ public Iterable<Integer> get(ChangeData input, FillArgs args)
+ throws OrmException {
+ return ImmutableSet.copyOf(Iterables.transform(input.draftsByUser(),
+ new Function<Account.Id, Integer>() {
+ @Override
+ public Integer apply(Account.Id account) {
+ return account.get();
+ }
+ }));
+ }
+ };
+
/**
* Users the change was reviewed by since the last author update.
* <p>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
index 4789a14..2eb210c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeSchemas.java
@@ -30,355 +30,9 @@
/** Secondary index schemas for changes. */
public class ChangeSchemas {
- @SuppressWarnings("deprecation")
- static final Schema<ChangeData> V14 = schema(
- ChangeField.LEGACY_ID,
- ChangeField.ID,
- ChangeField.STATUS,
- ChangeField.PROJECT,
- ChangeField.PROJECTS,
- ChangeField.REF,
- ChangeField.LEGACY_TOPIC2,
- ChangeField.UPDATED,
- ChangeField.FILE_PART,
- ChangeField.PATH,
- ChangeField.OWNER,
- ChangeField.REVIEWER,
- ChangeField.COMMIT,
- ChangeField.TR,
- ChangeField.LABEL,
- ChangeField.LEGACY_REVIEWED,
- ChangeField.COMMIT_MESSAGE,
- ChangeField.COMMENT,
- ChangeField.CHANGE,
- ChangeField.APPROVAL,
- ChangeField.MERGEABLE,
- ChangeField.ADDED,
- ChangeField.DELETED,
- ChangeField.DELTA,
- ChangeField.HASHTAG);
-
- @SuppressWarnings("deprecation")
- static final Schema<ChangeData> V15 = schema(
- ChangeField.LEGACY_ID,
- ChangeField.ID,
- ChangeField.STATUS,
- ChangeField.PROJECT,
- ChangeField.PROJECTS,
- ChangeField.REF,
- ChangeField.LEGACY_TOPIC2,
- ChangeField.UPDATED,
- ChangeField.FILE_PART,
- ChangeField.PATH,
- ChangeField.OWNER,
- ChangeField.REVIEWER,
- ChangeField.COMMIT,
- ChangeField.TR,
- ChangeField.LABEL,
- ChangeField.LEGACY_REVIEWED,
- ChangeField.COMMIT_MESSAGE,
- ChangeField.COMMENT,
- ChangeField.CHANGE,
- ChangeField.APPROVAL,
- ChangeField.MERGEABLE,
- ChangeField.ADDED,
- ChangeField.DELETED,
- ChangeField.DELTA,
- ChangeField.HASHTAG,
- ChangeField.COMMENTBY);
-
- @SuppressWarnings("deprecation")
- static final Schema<ChangeData> V16 = schema(
- ChangeField.LEGACY_ID,
- ChangeField.ID,
- ChangeField.STATUS,
- ChangeField.PROJECT,
- ChangeField.PROJECTS,
- ChangeField.REF,
- ChangeField.LEGACY_TOPIC3,
- ChangeField.UPDATED,
- ChangeField.FILE_PART,
- ChangeField.PATH,
- ChangeField.OWNER,
- ChangeField.REVIEWER,
- ChangeField.COMMIT,
- ChangeField.TR,
- ChangeField.LABEL,
- ChangeField.LEGACY_REVIEWED,
- ChangeField.COMMIT_MESSAGE,
- ChangeField.COMMENT,
- ChangeField.CHANGE,
- ChangeField.APPROVAL,
- ChangeField.MERGEABLE,
- ChangeField.ADDED,
- ChangeField.DELETED,
- ChangeField.DELTA,
- ChangeField.HASHTAG,
- ChangeField.COMMENTBY);
-
- @SuppressWarnings("deprecation")
- static final Schema<ChangeData> V17 = schema(
- ChangeField.LEGACY_ID,
- ChangeField.ID,
- ChangeField.STATUS,
- ChangeField.PROJECT,
- ChangeField.PROJECTS,
- ChangeField.REF,
- ChangeField.LEGACY_TOPIC3,
- ChangeField.UPDATED,
- ChangeField.FILE_PART,
- ChangeField.PATH,
- ChangeField.OWNER,
- ChangeField.REVIEWER,
- ChangeField.COMMIT,
- ChangeField.TR,
- ChangeField.LABEL,
- ChangeField.LEGACY_REVIEWED,
- ChangeField.COMMIT_MESSAGE,
- ChangeField.COMMENT,
- ChangeField.CHANGE,
- ChangeField.APPROVAL,
- ChangeField.MERGEABLE,
- ChangeField.ADDED,
- ChangeField.DELETED,
- ChangeField.DELTA,
- ChangeField.HASHTAG,
- ChangeField.COMMENTBY,
- ChangeField.PATCH_SET);
-
- @SuppressWarnings("deprecation")
- static final Schema<ChangeData> V18 = schema(
- ChangeField.LEGACY_ID,
- ChangeField.ID,
- ChangeField.STATUS,
- ChangeField.PROJECT,
- ChangeField.PROJECTS,
- ChangeField.REF,
- ChangeField.LEGACY_TOPIC3,
- ChangeField.UPDATED,
- ChangeField.FILE_PART,
- ChangeField.PATH,
- ChangeField.OWNER,
- ChangeField.REVIEWER,
- ChangeField.COMMIT,
- ChangeField.TR,
- ChangeField.LABEL,
- ChangeField.LEGACY_REVIEWED,
- ChangeField.COMMIT_MESSAGE,
- ChangeField.COMMENT,
- ChangeField.CHANGE,
- ChangeField.APPROVAL,
- ChangeField.MERGEABLE,
- ChangeField.ADDED,
- ChangeField.DELETED,
- ChangeField.DELTA,
- ChangeField.HASHTAG,
- ChangeField.COMMENTBY,
- ChangeField.PATCH_SET,
- ChangeField.GROUP);
-
- @SuppressWarnings("deprecation")
- static final Schema<ChangeData> V19 = schema(
- ChangeField.LEGACY_ID,
- ChangeField.ID,
- ChangeField.STATUS,
- ChangeField.PROJECT,
- ChangeField.PROJECTS,
- ChangeField.REF,
- ChangeField.LEGACY_TOPIC3,
- ChangeField.UPDATED,
- ChangeField.FILE_PART,
- ChangeField.PATH,
- ChangeField.OWNER,
- ChangeField.REVIEWER,
- ChangeField.COMMIT,
- ChangeField.TR,
- ChangeField.LABEL,
- ChangeField.LEGACY_REVIEWED,
- ChangeField.COMMIT_MESSAGE,
- ChangeField.COMMENT,
- ChangeField.CHANGE,
- ChangeField.APPROVAL,
- ChangeField.MERGEABLE,
- ChangeField.ADDED,
- ChangeField.DELETED,
- ChangeField.DELTA,
- ChangeField.HASHTAG,
- ChangeField.COMMENTBY,
- ChangeField.PATCH_SET,
- ChangeField.GROUP,
- ChangeField.EDITBY);
-
- @SuppressWarnings("deprecation")
- static final Schema<ChangeData> V20 = schema(
- ChangeField.LEGACY_ID,
- ChangeField.ID,
- ChangeField.STATUS,
- ChangeField.PROJECT,
- ChangeField.PROJECTS,
- ChangeField.REF,
- ChangeField.LEGACY_TOPIC3,
- ChangeField.UPDATED,
- ChangeField.FILE_PART,
- ChangeField.PATH,
- ChangeField.OWNER,
- ChangeField.REVIEWER,
- ChangeField.COMMIT,
- ChangeField.TR,
- ChangeField.LABEL,
- ChangeField.COMMIT_MESSAGE,
- ChangeField.COMMENT,
- ChangeField.CHANGE,
- ChangeField.APPROVAL,
- ChangeField.MERGEABLE,
- ChangeField.ADDED,
- ChangeField.DELETED,
- ChangeField.DELTA,
- ChangeField.HASHTAG,
- ChangeField.COMMENTBY,
- ChangeField.PATCH_SET,
- ChangeField.GROUP,
- ChangeField.EDITBY,
- ChangeField.REVIEWEDBY);
-
- @SuppressWarnings("deprecation")
- static final Schema<ChangeData> V21 = schema(
- ChangeField.LEGACY_ID,
- ChangeField.ID,
- ChangeField.STATUS,
- ChangeField.PROJECT,
- ChangeField.PROJECTS,
- ChangeField.REF,
- ChangeField.EXACT_TOPIC,
- ChangeField.FUZZY_TOPIC,
- ChangeField.UPDATED,
- ChangeField.FILE_PART,
- ChangeField.PATH,
- ChangeField.OWNER,
- ChangeField.REVIEWER,
- ChangeField.COMMIT,
- ChangeField.TR,
- ChangeField.LABEL,
- ChangeField.COMMIT_MESSAGE,
- ChangeField.COMMENT,
- ChangeField.CHANGE,
- ChangeField.APPROVAL,
- ChangeField.MERGEABLE,
- ChangeField.ADDED,
- ChangeField.DELETED,
- ChangeField.DELTA,
- ChangeField.HASHTAG,
- ChangeField.COMMENTBY,
- ChangeField.PATCH_SET,
- ChangeField.GROUP,
- ChangeField.EDITBY,
- ChangeField.REVIEWEDBY);
-
@Deprecated
- static final Schema<ChangeData> V22 = schema(
- ChangeField.LEGACY_ID,
- ChangeField.ID,
- ChangeField.STATUS,
- ChangeField.PROJECT,
- ChangeField.PROJECTS,
- ChangeField.REF,
- ChangeField.EXACT_TOPIC,
- ChangeField.FUZZY_TOPIC,
- ChangeField.UPDATED,
- ChangeField.FILE_PART,
- ChangeField.PATH,
- ChangeField.OWNER,
- ChangeField.REVIEWER,
- ChangeField.COMMIT,
- ChangeField.TR,
- ChangeField.LABEL,
- ChangeField.COMMIT_MESSAGE,
- ChangeField.COMMENT,
- ChangeField.CHANGE,
- ChangeField.APPROVAL,
- ChangeField.MERGEABLE,
- ChangeField.ADDED,
- ChangeField.DELETED,
- ChangeField.DELTA,
- ChangeField.HASHTAG,
- ChangeField.COMMENTBY,
- ChangeField.PATCH_SET,
- ChangeField.GROUP,
- ChangeField.EDITBY,
- ChangeField.REVIEWEDBY,
- ChangeField.EXACT_COMMIT);
-
- @Deprecated
- static final Schema<ChangeData> V23 = schema(
- ChangeField.LEGACY_ID2,
- ChangeField.ID,
- ChangeField.STATUS,
- ChangeField.PROJECT,
- ChangeField.PROJECTS,
- ChangeField.REF,
- ChangeField.EXACT_TOPIC,
- ChangeField.FUZZY_TOPIC,
- ChangeField.UPDATED,
- ChangeField.FILE_PART,
- ChangeField.PATH,
- ChangeField.OWNER,
- ChangeField.REVIEWER,
- ChangeField.COMMIT,
- ChangeField.TR,
- ChangeField.LABEL,
- ChangeField.COMMIT_MESSAGE,
- ChangeField.COMMENT,
- ChangeField.CHANGE,
- ChangeField.APPROVAL,
- ChangeField.MERGEABLE,
- ChangeField.ADDED,
- ChangeField.DELETED,
- ChangeField.DELTA,
- ChangeField.HASHTAG,
- ChangeField.COMMENTBY,
- ChangeField.PATCH_SET,
- ChangeField.GROUP,
- ChangeField.EDITBY,
- ChangeField.REVIEWEDBY,
- ChangeField.EXACT_COMMIT);
-
- static final Schema<ChangeData> V24 = schema(
- ChangeField.LEGACY_ID2,
- ChangeField.ID,
- ChangeField.STATUS,
- ChangeField.PROJECT,
- ChangeField.PROJECTS,
- ChangeField.REF,
- ChangeField.EXACT_TOPIC,
- ChangeField.FUZZY_TOPIC,
- ChangeField.UPDATED,
- ChangeField.FILE_PART,
- ChangeField.PATH,
- ChangeField.OWNER,
- ChangeField.REVIEWER,
- ChangeField.COMMIT,
- ChangeField.TR,
- ChangeField.LABEL,
- ChangeField.COMMIT_MESSAGE,
- ChangeField.COMMENT,
- ChangeField.CHANGE,
- ChangeField.APPROVAL,
- ChangeField.MERGEABLE,
- ChangeField.ADDED,
- ChangeField.DELETED,
- ChangeField.DELTA,
- ChangeField.HASHTAG,
- ChangeField.COMMENTBY,
- ChangeField.PATCH_SET,
- ChangeField.GROUP,
- ChangeField.EDITBY,
- ChangeField.REVIEWEDBY,
- ChangeField.EXACT_COMMIT,
- ChangeField.AUTHOR,
- ChangeField.COMMITTER);
-
static final Schema<ChangeData> V25 = schema(
- ChangeField.LEGACY_ID2,
+ ChangeField.LEGACY_ID,
ChangeField.ID,
ChangeField.STATUS,
ChangeField.PROJECT,
@@ -413,6 +67,43 @@
ChangeField.AUTHOR,
ChangeField.COMMITTER);
+ static final Schema<ChangeData> V26 = schema(
+ ChangeField.LEGACY_ID,
+ ChangeField.ID,
+ ChangeField.STATUS,
+ ChangeField.PROJECT,
+ ChangeField.PROJECTS,
+ ChangeField.REF,
+ ChangeField.EXACT_TOPIC,
+ ChangeField.FUZZY_TOPIC,
+ ChangeField.UPDATED,
+ ChangeField.FILE_PART,
+ ChangeField.PATH,
+ ChangeField.OWNER,
+ ChangeField.REVIEWER,
+ ChangeField.COMMIT,
+ ChangeField.TR,
+ ChangeField.LABEL,
+ ChangeField.COMMIT_MESSAGE,
+ ChangeField.COMMENT,
+ ChangeField.CHANGE,
+ ChangeField.APPROVAL,
+ ChangeField.MERGEABLE,
+ ChangeField.ADDED,
+ ChangeField.DELETED,
+ ChangeField.DELTA,
+ ChangeField.HASHTAG,
+ ChangeField.COMMENTBY,
+ ChangeField.PATCH_SET,
+ ChangeField.GROUP,
+ ChangeField.SUBMISSIONID,
+ ChangeField.EDITBY,
+ ChangeField.REVIEWEDBY,
+ ChangeField.EXACT_COMMIT,
+ ChangeField.AUTHOR,
+ ChangeField.COMMITTER,
+ ChangeField.DRAFTBY);
+
private static Schema<ChangeData> schema(Collection<FieldDef<ChangeData, ?>> fields) {
return new Schema<>(ImmutableList.copyOf(fields));
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index 5222544..93ffbe2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -335,7 +335,7 @@
// If we have nobody to send this message to, then all of our
// selection filters previously for this type of message were
// unable to match a destination. Don't bother sending it.
- log.info("Skipping delivery of email with no recipients");
+ log.warn("Skipping delivery of email with no recipients");
return false;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 22c8da4..61c9aa4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -117,10 +117,9 @@
throws IOException {
if (migration.writeChanges()) {
load();
- MetaDataUpdate md =
- updateFactory.create(getProjectName(),
- repoManager.openMetadataRepository(getProjectName()), getUser(),
- bru);
+ Project.NameKey p = getProjectName();
+ MetaDataUpdate md = updateFactory.create(
+ p, repoManager.openMetadataRepository(p), getUser(), bru);
md.setAllowEmpty(true);
return super.openUpdate(md);
}
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 9d759ed..3bca5d5 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
@@ -27,6 +27,7 @@
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
@@ -38,6 +39,7 @@
import com.google.common.collect.Table;
import com.google.common.collect.Tables;
import com.google.common.primitives.Ints;
+import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
@@ -85,8 +87,9 @@
private final ObjectId tip;
private final RevWalk walk;
private final Repository repo;
- private final Map<PatchSet.Id,
- Table<Account.Id, String, Optional<PatchSetApproval>>> approvals;
+ private final Map<PatchSet.Id, Table<Account.Id, String, PatchSetApproval>>
+ approvals;
+ private final Map<PatchSet.Id, Multimap<Account.Id, String>> removedApprovals;
private final List<ChangeMessage> allChangeMessages;
private final Multimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet;
@@ -99,6 +102,7 @@
this.repo =
repoManager.openMetadataRepository(ChangeNotes.getProjectName(change));
approvals = Maps.newHashMap();
+ removedApprovals = Maps.newHashMap();
reviewers = Maps.newLinkedHashMap();
allPastReviewers = Lists.newArrayList();
submitRecords = Lists.newArrayListWithExpectedSize(1);
@@ -126,9 +130,8 @@
buildApprovals() {
Multimap<PatchSet.Id, PatchSetApproval> result =
ArrayListMultimap.create(approvals.keySet().size(), 3);
- for (Table<?, ?, Optional<PatchSetApproval>> curr
- : approvals.values()) {
- for (PatchSetApproval psa : Optional.presentInstances(curr.values())) {
+ for (Table<?, ?, PatchSetApproval> curr : approvals.values()) {
+ for (PatchSetApproval psa : curr.values()) {
result.put(psa.getPatchSetId(), psa);
}
}
@@ -305,46 +308,99 @@
private void parseApproval(PatchSet.Id psId, Account.Id accountId,
RevCommit commit, String line) throws ConfigInvalidException {
- Table<Account.Id, String, Optional<PatchSetApproval>> curr =
- approvals.get(psId);
- if (curr == null) {
- curr = Tables.newCustomTable(
- Maps.<Account.Id, Map<String, Optional<PatchSetApproval>>>
+ if (line.startsWith("-")) {
+ parseRemoveApproval(psId, accountId, line);
+ } else {
+ parseAddApproval(psId, accountId, commit, line);
+ }
+ }
+
+ private void parseAddApproval(PatchSet.Id psId, Account.Id accountId,
+ RevCommit commit, String line) throws ConfigInvalidException {
+ LabelVote l;
+ try {
+ l = LabelVote.parseWithEquals(line);
+ } catch (IllegalArgumentException e) {
+ ConfigInvalidException pe =
+ parseException("invalid %s: %s", FOOTER_LABEL, line);
+ pe.initCause(e);
+ throw pe;
+ }
+ if (isApprovalRemoved(psId, accountId, l.label())) {
+ return;
+ }
+
+ Table<Account.Id, String, PatchSetApproval> curr = approvals.get(psId);
+ if (curr != null) {
+ if (curr.contains(accountId, l.label())) {
+ return;
+ }
+ } else {
+ curr = newApprovalsTable();
+ approvals.put(psId, curr);
+ }
+ curr.put(accountId, l.label(), new PatchSetApproval(
+ new PatchSetApproval.Key(
+ psId,
+ accountId,
+ new LabelId(l.label())),
+ l.value(),
+ new Timestamp(commit.getCommitterIdent().getWhen().getTime())));
+ }
+
+ private static Table<Account.Id, String, PatchSetApproval>
+ newApprovalsTable() {
+ return Tables.newCustomTable(
+ Maps.<Account.Id, Map<String, PatchSetApproval>>
newHashMapWithExpectedSize(2),
- new Supplier<Map<String, Optional<PatchSetApproval>>>() {
+ new Supplier<Map<String, PatchSetApproval>>() {
@Override
- public Map<String, Optional<PatchSetApproval>> get() {
+ public Map<String, PatchSetApproval> get() {
return Maps.newLinkedHashMap();
}
});
- approvals.put(psId, curr);
+ }
+
+ private void parseRemoveApproval(PatchSet.Id psId, Account.Id accountId,
+ String line) throws ConfigInvalidException {
+ Multimap<Account.Id, String> curr = removedApprovals.get(psId);
+ if (curr == null) {
+ curr = HashMultimap.create(1, 1);
+ removedApprovals.put(psId, curr);
+ }
+ String label;
+ Account.Id removedAccountId;
+ line = line.substring(1);
+ int s = line.indexOf(' ');
+ if (s > 0) {
+ label = line.substring(0, s);
+ PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1));
+ checkFooter(ident != null, FOOTER_LABEL, line);
+ removedAccountId = parseIdent(ident);
+ } else {
+ label = line;
+ removedAccountId = accountId;
}
- if (line.startsWith("-")) {
- String label = line.substring(1);
- if (!curr.contains(accountId, label)) {
- curr.put(accountId, label, Optional.<PatchSetApproval> absent());
- }
- } else {
- LabelVote l;
- try {
- l = LabelVote.parseWithEquals(line);
- } catch (IllegalArgumentException e) {
- ConfigInvalidException pe =
- parseException("invalid %s: %s", FOOTER_LABEL, line);
- pe.initCause(e);
- throw pe;
- }
- if (!curr.contains(accountId, l.label())) {
- curr.put(accountId, l.label(), Optional.of(new PatchSetApproval(
- new PatchSetApproval.Key(
- psId,
- accountId,
- new LabelId(l.label())),
- l.value(),
- new Timestamp(commit.getCommitterIdent().getWhen().getTime()))));
- }
+ Table<Account.Id, String, PatchSetApproval> added = approvals.get(psId);
+ if (added != null && added.contains(accountId, label)) {
+ return;
}
+
+ try {
+ curr.put(removedAccountId, LabelType.checkNameInternal(label));
+ } catch (IllegalArgumentException e) {
+ ConfigInvalidException pe =
+ parseException("invalid %s: %s", FOOTER_LABEL, line);
+ pe.initCause(e);
+ throw pe;
+ }
+ }
+
+ private boolean isApprovalRemoved(PatchSet.Id psId, Account.Id accountId,
+ String label) {
+ Multimap<Account.Id, String> curr = removedApprovals.get(psId);
+ return curr != null && curr.containsEntry(accountId, label);
}
private void parseSubmitRecords(List<String> lines)
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 7c011b6..f1843bd 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
@@ -25,11 +25,12 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
-import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.MultimapBuilder;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
@@ -87,8 +88,9 @@
}
private final AccountCache accountCache;
- private final Map<String, Optional<Short>> approvals;
+ private final Map<String, Short> approvals;
private final Map<Account.Id, ReviewerStateInternal> reviewers;
+ private final Multimap<Account.Id, String> removedApprovals;
private Change.Status status;
private String subject;
private List<SubmitRecord> submitRecords;
@@ -163,6 +165,8 @@
this.commentsUtil = commentsUtil;
this.approvals = Maps.newTreeMap(labelNameComparator);
this.reviewers = Maps.newLinkedHashMap();
+ this.removedApprovals =
+ MultimapBuilder.linkedHashKeys(1).arrayListValues(1).build();
this.comments = Lists.newArrayList();
}
@@ -177,11 +181,15 @@
}
public void putApproval(String label, short value) {
- approvals.put(label, Optional.of(value));
+ approvals.put(label, value);
}
public void removeApproval(String label) {
- approvals.put(label, Optional.<Short> absent());
+ removeApprovalFor(getUser().getAccountId(), label);
+ }
+
+ public void removeApprovalFor(Account.Id reviewer, String label) {
+ removedApprovals.put(reviewer, label);
}
public void merge(Iterable<SubmitRecord> submitRecords) {
@@ -434,20 +442,22 @@
}
for (Map.Entry<Account.Id, ReviewerStateInternal> e : reviewers.entrySet()) {
- Account account = accountCache.get(e.getKey()).getAccount();
- PersonIdent ident = newIdent(account, when);
- addFooter(msg, e.getValue().getFooterKey())
- .append(ident.getName())
- .append(" <").append(ident.getEmailAddress()).append(">\n");
+ addFooter(msg, e.getValue().getFooterKey());
+ addIdent(msg, e.getKey()).append('\n');
}
- for (Map.Entry<String, Optional<Short>> e : approvals.entrySet()) {
- if (!e.getValue().isPresent()) {
- addFooter(msg, FOOTER_LABEL, '-', e.getKey());
- } else {
- addFooter(msg, FOOTER_LABEL, LabelVote.create(
- e.getKey(), e.getValue().get()).formatWithEquals());
+ for (Map.Entry<String, Short> e : approvals.entrySet()) {
+ addFooter(msg, FOOTER_LABEL, LabelVote.create(
+ e.getKey(), e.getValue()).formatWithEquals());
+ }
+
+ for (Map.Entry<Account.Id, String> e : removedApprovals.entries()) {
+ addFooter(msg, FOOTER_LABEL).append('-').append(e.getValue());
+ Account.Id id = e.getKey();
+ if (!id.equals(ctl.getUser().getAccountId())) {
+ addIdent(msg.append(' '), id);
}
+ msg.append('\n');
}
if (submitRecords != null) {
@@ -486,6 +496,7 @@
private boolean isEmpty() {
return approvals.isEmpty()
+ && removedApprovals.isEmpty()
&& changeMessage == null
&& comments.isEmpty()
&& reviewers.isEmpty()
@@ -509,6 +520,14 @@
sb.append('\n');
}
+ private StringBuilder addIdent(StringBuilder sb, Account.Id accountId) {
+ Account account = accountCache.get(accountId).getAccount();
+ PersonIdent ident = newIdent(account, when);
+ sb.append(ident.getName()).append(" <").append(ident.getEmailAddress())
+ .append('>');
+ return sb;
+ }
+
private static String sanitizeFooter(String value) {
return value.replace('\n', ' ').replace('\0', ' ');
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
index 8740a6b..e479ba9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
@@ -26,6 +26,7 @@
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
@@ -56,7 +57,11 @@
if (patchList.isAgainstParent()) {
a = Text.EMPTY;
} else {
- a = Text.forCommit(reader, patchList.getOldId());
+ // For the initial commit, we have an empty tree on Side A
+ RevObject object = rw.parseAny(patchList.getOldId());
+ a = object instanceof RevCommit
+ ? Text.forCommit(reader, object)
+ : Text.EMPTY;
}
b = Text.forCommit(reader, bCommit);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index c4b686c..c460fd0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -284,9 +284,7 @@
}
private void createProjectConfig(CreateProjectArgs args) throws IOException, ConfigInvalidException {
- MetaDataUpdate md =
- metaDataUpdateFactory.create(args.getProject());
- try {
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(args.getProject())) {
ProjectConfig config = ProjectConfig.read(md);
config.load(md);
@@ -321,8 +319,6 @@
md.setMessage("Created project\n");
config.commit(md);
- } finally {
- md.close();
}
projectCache.onCreateProject(args.getProject());
repoManager.setProjectDescription(args.getProject(),
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 5f921b1..fd7539f 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
@@ -139,15 +139,7 @@
throw new BadRequestException("config is required");
}
- final MetaDataUpdate md;
- try {
- md = metaDataUpdateFactory.get().create(projectName);
- } catch (RepositoryNotFoundException notFound) {
- throw new ResourceNotFoundException(projectName.get());
- } catch (IOException e) {
- throw new ResourceNotFoundException(projectName.get(), e);
- }
- try {
+ try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
ProjectConfig projectConfig = ProjectConfig.read(md);
Project p = projectConfig.getProject();
@@ -226,12 +218,12 @@
return new ConfigInfo(serverEnableSignedPush,
state.controlFor(user.get()), config, pluginConfigEntries,
cfgFactory, allProjects, views);
+ } catch (RepositoryNotFoundException notFound) {
+ throw new ResourceNotFoundException(projectName.get());
} catch (ConfigInvalidException err) {
throw new ResourceConflictException("Cannot read project " + projectName, err);
} catch (IOException err) {
throw new ResourceConflictException("Cannot update project " + projectName, err);
- } finally {
- md.close();
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
index 2d407bd..7cf426d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
@@ -76,42 +76,37 @@
throw new AuthException("not project owner");
}
- try {
- MetaDataUpdate md = updateFactory.create(resource.getNameKey());
- try {
- ProjectConfig config = ProjectConfig.read(md);
- Project project = config.getProject();
- project.setDescription(Strings.emptyToNull(input.description));
+ try (MetaDataUpdate md = updateFactory.create(resource.getNameKey())) {
+ ProjectConfig config = ProjectConfig.read(md);
+ Project project = config.getProject();
+ project.setDescription(Strings.emptyToNull(input.description));
- String msg = MoreObjects.firstNonNull(
- Strings.emptyToNull(input.commitMessage),
- "Updated description.\n");
- if (!msg.endsWith("\n")) {
- msg += "\n";
- }
- md.setAuthor(user);
- md.setMessage(msg);
- ObjectId baseRev = config.getRevision();
- ObjectId commitRev = config.commit(md);
- // Only fire hook if project was actually changed.
- if (!Objects.equals(baseRev, commitRev)) {
- gitRefUpdated.fire(resource.getNameKey(), RefNames.REFS_CONFIG,
- baseRev, commitRev);
- hooks.doRefUpdatedHook(
- new Branch.NameKey(resource.getNameKey(), RefNames.REFS_CONFIG),
- baseRev, commitRev, user.getAccount());
- }
- cache.evict(ctl.getProject());
- gitMgr.setProjectDescription(
- resource.getNameKey(),
- project.getDescription());
-
- return Strings.isNullOrEmpty(project.getDescription())
- ? Response.<String>none()
- : Response.ok(project.getDescription());
- } finally {
- md.close();
+ String msg = MoreObjects.firstNonNull(
+ Strings.emptyToNull(input.commitMessage),
+ "Updated description.\n");
+ if (!msg.endsWith("\n")) {
+ msg += "\n";
}
+ md.setAuthor(user);
+ md.setMessage(msg);
+ ObjectId baseRev = config.getRevision();
+ ObjectId commitRev = config.commit(md);
+ // Only fire hook if project was actually changed.
+ if (!Objects.equals(baseRev, commitRev)) {
+ gitRefUpdated.fire(resource.getNameKey(), RefNames.REFS_CONFIG,
+ baseRev, commitRev);
+ hooks.doRefUpdatedHook(
+ new Branch.NameKey(resource.getNameKey(), RefNames.REFS_CONFIG),
+ baseRev, commitRev, user.getAccount());
+ }
+ cache.evict(ctl.getProject());
+ gitMgr.setProjectDescription(
+ resource.getNameKey(),
+ project.getDescription());
+
+ return Strings.isNullOrEmpty(project.getDescription())
+ ? Response.<String>none()
+ : Response.ok(project.getDescription());
} catch (RepositoryNotFoundException notFound) {
throw new ResourceNotFoundException(resource.getName());
} catch (ConfigInvalidException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
index ac01de5..641c3a7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
@@ -84,39 +84,34 @@
}
}
- try {
- MetaDataUpdate md = updateFactory.create(ctl.getProject().getNameKey());
- try {
- ProjectConfig config = ProjectConfig.read(md);
- Project project = config.getProject();
- if (inherited) {
- project.setDefaultDashboard(input.id);
- } else {
- project.setLocalDefaultDashboard(input.id);
- }
-
- String msg = MoreObjects.firstNonNull(
- Strings.emptyToNull(input.commitMessage),
- input.id == null
- ? "Removed default dashboard.\n"
- : String.format("Changed default dashboard to %s.\n", input.id));
- if (!msg.endsWith("\n")) {
- msg += "\n";
- }
- md.setAuthor(ctl.getUser().asIdentifiedUser());
- md.setMessage(msg);
- config.commit(md);
- cache.evict(ctl.getProject());
-
- if (target != null) {
- DashboardInfo info = get.get().apply(target);
- info.isDefault = true;
- return Response.ok(info);
- }
- return Response.none();
- } finally {
- md.close();
+ try (MetaDataUpdate md = updateFactory.create(ctl.getProject().getNameKey())) {
+ ProjectConfig config = ProjectConfig.read(md);
+ Project project = config.getProject();
+ if (inherited) {
+ project.setDefaultDashboard(input.id);
+ } else {
+ project.setLocalDefaultDashboard(input.id);
}
+
+ String msg = MoreObjects.firstNonNull(
+ Strings.emptyToNull(input.commitMessage),
+ input.id == null
+ ? "Removed default dashboard.\n"
+ : String.format("Changed default dashboard to %s.\n", input.id));
+ if (!msg.endsWith("\n")) {
+ msg += "\n";
+ }
+ md.setAuthor(ctl.getUser().asIdentifiedUser());
+ md.setMessage(msg);
+ config.commit(md);
+ cache.evict(ctl.getProject());
+
+ if (target != null) {
+ DashboardInfo info = get.get().apply(target);
+ info.isDefault = true;
+ return Response.ok(info);
+ }
+ return Response.none();
} catch (RepositoryNotFoundException notFound) {
throw new ResourceNotFoundException(ctl.getProject().getName());
} catch (ConfigInvalidException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
index 15c977f..8113c01 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -71,32 +71,27 @@
ResourceNotFoundException, UnprocessableEntityException, IOException {
ProjectControl ctl = rsrc.getControl();
validateParentUpdate(ctl, input.parent, checkIfAdmin);
- try {
- MetaDataUpdate md = updateFactory.create(rsrc.getNameKey());
- try {
- ProjectConfig config = ProjectConfig.read(md);
- Project project = config.getProject();
- project.setParentName(Strings.emptyToNull(input.parent));
+ try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) {
+ ProjectConfig config = ProjectConfig.read(md);
+ Project project = config.getProject();
+ project.setParentName(Strings.emptyToNull(input.parent));
- String msg = Strings.emptyToNull(input.commitMessage);
- if (msg == null) {
- msg = String.format(
- "Changed parent to %s.\n",
- MoreObjects.firstNonNull(project.getParentName(),
- allProjects.get()));
- } else if (!msg.endsWith("\n")) {
- msg += "\n";
- }
- md.setAuthor(ctl.getUser().asIdentifiedUser());
- md.setMessage(msg);
- config.commit(md);
- cache.evict(ctl.getProject());
-
- Project.NameKey parentName = project.getParent(allProjects);
- return parentName != null ? parentName.get() : "";
- } finally {
- md.close();
+ String msg = Strings.emptyToNull(input.commitMessage);
+ if (msg == null) {
+ msg = String.format(
+ "Changed parent to %s.\n",
+ MoreObjects.firstNonNull(project.getParentName(),
+ allProjects.get()));
+ } else if (!msg.endsWith("\n")) {
+ msg += "\n";
}
+ md.setAuthor(ctl.getUser().asIdentifiedUser());
+ md.setMessage(msg);
+ config.commit(md);
+ cache.evict(ctl.getProject());
+
+ Project.NameKey parentName = project.getParent(allProjects);
+ return parentName != null ? parentName.get() : "";
} catch (RepositoryNotFoundException notFound) {
throw new ResourceNotFoundException(rsrc.getName());
} catch (ConfigInvalidException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index 2b35cef..2ea7e82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -73,14 +73,7 @@
}
public static SubmitTypeRecord defaultTypeError() {
- return createTypeError(DEFAULT_MSG);
- }
-
- public static SubmitTypeRecord createTypeError(String err) {
- SubmitTypeRecord rec = new SubmitTypeRecord();
- rec.status = SubmitTypeRecord.Status.RULE_ERROR;
- rec.errorMessage = err;
- return rec;
+ return SubmitTypeRecord.error(DEFAULT_MSG);
}
/**
@@ -231,7 +224,8 @@
// required for this change to be submittable. Each label will indicate
// whether or not that is actually possible given the permissions.
return ruleError(String.format("Submit rule '%s' for change %s of %s has "
- + "no solution.", getSubmitRule(), cd.getId(), getProjectName()));
+ + "no solution.", getSubmitRuleName(), cd.getId(),
+ getProjectName()));
}
return resultsToSubmitRecord(getSubmitRule(), results);
@@ -241,7 +235,9 @@
try {
if (!control.isDraftVisible(cd.db(), cd)) {
return createRuleError("Patch set " + patchSet.getId() + " not found");
- } else if (patchSet.isDraft()) {
+ }
+ initPatchSet();
+ if (patchSet.isDraft()) {
return createRuleError("Cannot submit draft patch sets");
} else {
return createRuleError("Cannot submit draft changes");
@@ -385,15 +381,17 @@
try {
if (control.getChange().getStatus() == Change.Status.DRAFT
&& !control.isDraftVisible(cd.db(), cd)) {
- return createTypeError("Patch set " + patchSet.getId() + " not found");
+ return SubmitTypeRecord.error(
+ "Patch set " + patchSet.getId() + " not found");
}
if (patchSet.isDraft() && !control.isDraftVisible(cd.db(), cd)) {
- return createTypeError("Patch set " + patchSet.getId() + " not found");
+ return SubmitTypeRecord.error(
+ "Patch set " + patchSet.getId() + " not found");
}
} catch (OrmException err) {
String msg = "Cannot read patch set " + patchSet.getId();
log.error(msg, err);
- return createTypeError(msg);
+ return SubmitTypeRecord.error(msg);
}
List<Term> results;
@@ -410,13 +408,13 @@
if (results.isEmpty()) {
// Should never occur for a well written rule
- return typeError("Submit rule '" + getSubmitRule() + "' for change "
+ return typeError("Submit rule '" + getSubmitRuleName() + "' for change "
+ cd.getId() + " of " + getProjectName() + " has no solution.");
}
Term typeTerm = results.get(0);
if (!(typeTerm instanceof SymbolTerm)) {
- return typeError("Submit rule '" + getSubmitRule() + "' for change "
+ return typeError("Submit rule '" + getSubmitRuleName() + "' for change "
+ cd.getId() + " of " + getProjectName()
+ " did not return a symbol.");
}
@@ -445,7 +443,7 @@
}
return defaultTypeError();
} else {
- return createTypeError(err);
+ return SubmitTypeRecord.error(err);
}
}
@@ -609,6 +607,10 @@
return submitRule;
}
+ public String getSubmitRuleName() {
+ return submitRule != null ? submitRule.toString() : "<unknown rule>";
+ }
+
private void initPatchSet() throws OrmException {
if (patchSet == null) {
patchSet = cd.currentPatchSet();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
index 193a061..6264f3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
@@ -23,7 +23,7 @@
public class AuthorPredicate extends IndexPredicate<ChangeData> {
AuthorPredicate(String value) {
- super(AUTHOR, FIELD_AUTHOR, value);
+ super(AUTHOR, FIELD_AUTHOR, value.toLowerCase());
}
@Override
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 a3fb523..1f7bd9b 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
@@ -316,9 +316,11 @@
private List<ChangeMessage> messages;
private List<SubmitRecord> submitRecords;
private ChangedLines changedLines;
+ private SubmitTypeRecord submitTypeRecord;
private Boolean mergeable;
private Set<Account.Id> editsByUser;
private Set<Account.Id> reviewedBy;
+ private Set<Account.Id> draftsByUser;
private PersonIdent author;
private PersonIdent committer;
@@ -753,6 +755,13 @@
return submitRecords;
}
+ public SubmitTypeRecord submitTypeRecord() throws OrmException {
+ if (submitTypeRecord == null) {
+ submitTypeRecord = new SubmitRuleEvaluator(this).getSubmitType();
+ }
+ return submitTypeRecord;
+ }
+
public void setMergeable(Boolean mergeable) {
this.mergeable = mergeable;
}
@@ -772,18 +781,18 @@
}
try (Repository repo = repoManager.openRepository(c.getProject())) {
Ref ref = repo.getRefDatabase().exactRef(c.getDest().get());
- SubmitTypeRecord rec = new SubmitRuleEvaluator(this)
- .getSubmitType();
- if (rec.status != SubmitTypeRecord.Status.OK) {
- throw new OrmException(
- "Error in mergeability check: " + rec.errorMessage);
+ SubmitTypeRecord str = submitTypeRecord();
+ if (!str.isOk()) {
+ // If submit type rules are broken, it's definitely not mergeable.
+ // No need to log, as SubmitRuleEvaluator already did it for us.
+ return false;
}
String mergeStrategy = mergeUtilFactory
.create(projectCache.get(c.getProject()))
.mergeStrategyName();
mergeable = mergeabilityCache.get(
ObjectId.fromString(ps.getRevision().get()),
- ref, rec.type, mergeStrategy, c.getDest(), repo, db);
+ ref, str.type, mergeStrategy, c.getDest(), repo);
} catch (IOException e) {
throw new OrmException(e);
}
@@ -814,6 +823,20 @@
return editsByUser;
}
+ public Set<Account.Id> draftsByUser() throws OrmException {
+ if (draftsByUser == null) {
+ Change c = change();
+ if (c == null) {
+ return Collections.emptySet();
+ }
+ draftsByUser = new HashSet<>();
+ for (PatchLineComment sc : plcUtil.draftByChange(db, notes)) {
+ draftsByUser.add(sc.getAuthor());
+ }
+ }
+ return draftsByUser;
+ }
+
public Set<Account.Id> reviewedBy() throws OrmException {
if (reviewedBy == null) {
Change c = change();
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 440731e..9057d82 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
@@ -48,8 +48,9 @@
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
+import com.google.gerrit.server.git.strategy.SubmitDryRun;
import com.google.gerrit.server.group.ListMembers;
+import com.google.gerrit.server.index.ChangeField;
import com.google.gerrit.server.index.ChangeIndex;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.IndexCollection;
@@ -171,7 +172,7 @@
final GitRepositoryManager repoManager;
final ProjectCache projectCache;
final Provider<ListChildProjects> listChildProjects;
- final SubmitStrategyFactory submitStrategyFactory;
+ final SubmitDryRun submitDryRun;
final ConflictsCache conflictsCache;
final TrackingFooters trackingFooters;
final ChangeIndex index;
@@ -203,7 +204,7 @@
ProjectCache projectCache,
Provider<ListChildProjects> listChildProjects,
IndexCollection indexes,
- SubmitStrategyFactory submitStrategyFactory,
+ SubmitDryRun submitDryRun,
ConflictsCache conflictsCache,
TrackingFooters trackingFooters,
IndexConfig indexConfig,
@@ -213,7 +214,7 @@
capabilityControlFactory, changeControlGenericFactory,
changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
allProjectsName, allUsersName, patchListCache, repoManager,
- projectCache, listChildProjects, submitStrategyFactory,
+ projectCache, listChildProjects, submitDryRun,
conflictsCache, trackingFooters,
indexes != null ? indexes.getSearchIndex() : null,
indexConfig, listMembers,
@@ -240,7 +241,7 @@
GitRepositoryManager repoManager,
ProjectCache projectCache,
Provider<ListChildProjects> listChildProjects,
- SubmitStrategyFactory submitStrategyFactory,
+ SubmitDryRun submitDryRun,
ConflictsCache conflictsCache,
TrackingFooters trackingFooters,
ChangeIndex index,
@@ -266,7 +267,7 @@
this.repoManager = repoManager;
this.projectCache = projectCache;
this.listChildProjects = listChildProjects;
- this.submitStrategyFactory = submitStrategyFactory;
+ this.submitDryRun = submitDryRun;
this.conflictsCache = conflictsCache;
this.trackingFooters = trackingFooters;
this.index = index;
@@ -281,7 +282,7 @@
capabilityControlFactory, changeControlGenericFactory,
changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
allProjectsName, allUsersName, patchListCache, repoManager,
- projectCache, listChildProjects, submitStrategyFactory,
+ projectCache, listChildProjects, submitDryRun,
conflictsCache, trackingFooters, index, indexConfig, listMembers,
allowsDrafts);
}
@@ -379,8 +380,7 @@
@Operator
public Predicate<ChangeData> change(String query) throws QueryParseException {
if (PAT_LEGACY_ID.matcher(query).matches()) {
- return new LegacyChangeIdPredicate(
- args.getSchema(), Change.Id.parse(query));
+ return new LegacyChangeIdPredicate(Change.Id.parse(query));
} else if (PAT_CHANGE_ID.matcher(query).matches()) {
return new ChangeIdPredicate(parseChangeId(query));
}
@@ -403,7 +403,7 @@
@Operator
public Predicate<ChangeData> status(String statusName) {
if ("reviewed".equalsIgnoreCase(statusName)) {
- return IsReviewedPredicate.create(args.getSchema());
+ return IsReviewedPredicate.create();
} else {
return ChangeStatusPredicate.parse(statusName);
}
@@ -420,7 +420,7 @@
}
if ("draft".equalsIgnoreCase(value)) {
- return new HasDraftByPredicate(args, self());
+ return draftby(self());
}
if ("edit".equalsIgnoreCase(value)) {
@@ -444,7 +444,7 @@
}
if ("reviewed".equalsIgnoreCase(value)) {
- return IsReviewedPredicate.create(args.getSchema());
+ return IsReviewedPredicate.create();
}
if ("owner".equalsIgnoreCase(value)) {
@@ -518,18 +518,18 @@
@Operator
public Predicate<ChangeData> topic(String name) {
- return new ExactTopicPredicate(args.getSchema(), name);
+ return new ExactTopicPredicate(name);
}
@Operator
public Predicate<ChangeData> intopic(String name) {
if (name.startsWith("^")) {
- return new RegexTopicPredicate(args.getSchema(), name);
+ return new RegexTopicPredicate(name);
}
if (name.isEmpty()) {
- return new ExactTopicPredicate(args.getSchema(), name);
+ return new ExactTopicPredicate(name);
}
- return new FuzzyTopicPredicate(args.getSchema(), name, args.index);
+ return new FuzzyTopicPredicate(name, args.index);
}
@Operator
@@ -685,13 +685,19 @@
public Predicate<ChangeData> draftby(String who) throws QueryParseException,
OrmException {
Set<Account.Id> m = parseAccount(who);
- List<HasDraftByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+ List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
for (Account.Id id : m) {
- p.add(new HasDraftByPredicate(args, id));
+ p.add(draftby(id));
}
return Predicate.or(p);
}
+ private Predicate<ChangeData> draftby(Account.Id who) {
+ return args.getSchema().hasField(ChangeField.DRAFTBY)
+ ? new HasDraftByPredicate(who)
+ : new HasDraftByLegacyPredicate(args, who);
+ }
+
@Operator
public Predicate<ChangeData> visibleto(String who)
throws QueryParseException, OrmException {
@@ -870,7 +876,7 @@
@Operator
public Predicate<ChangeData> reviewedby(String who)
throws QueryParseException, OrmException {
- return IsReviewedPredicate.create(args.getSchema(), parseAccount(who));
+ return IsReviewedPredicate.create(parseAccount(who));
}
@Operator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
index dd3c3b3..756715b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -33,8 +33,7 @@
public boolean match(ChangeData object) throws OrmException {
try {
Predicate<ChangeData> p = Predicate.and(
- new LegacyChangeIdPredicate(index.getSchema(), object.getId()),
- this);
+ new LegacyChangeIdPredicate(object.getId()), this);
for (ChangeData cData
: index.getSource(p, QueryOptions.oneResult()).read()) {
if (cData.getId().equals(object.getId())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
index e5d9529..3cb7f8e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
@@ -23,7 +23,7 @@
public class CommitterPredicate extends IndexPredicate<ChangeData> {
CommitterPredicate(String value) {
- super(COMMITTER, FIELD_COMMITTER, value);
+ super(COMMITTER, FIELD_COMMITTER, value.toLowerCase());
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 9b47302..1977847 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -15,19 +15,16 @@
package com.google.gerrit.server.query.change;
import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
import com.google.gerrit.common.data.SubmitTypeRecord;
-import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb;
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.SubmitStrategy;
+import com.google.gerrit.server.git.strategy.SubmitDryRun;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.OperatorPredicate;
import com.google.gerrit.server.query.OrPredicate;
import com.google.gerrit.server.query.Predicate;
@@ -35,13 +32,10 @@
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.lib.Constants;
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.RevFlag;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.treewalk.TreeWalk;
@@ -49,6 +43,7 @@
import java.io.IOException;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -80,7 +75,7 @@
List<Predicate<ChangeData>> predicatesForOneChange =
Lists.newArrayListWithCapacity(5);
predicatesForOneChange.add(
- not(new LegacyChangeIdPredicate(args.getSchema(), c.getId())));
+ not(new LegacyChangeIdPredicate(c.getId())));
predicatesForOneChange.add(
new ProjectPredicate(c.getProject().get()));
predicatesForOneChange.add(
@@ -101,14 +96,14 @@
if (!otherChange.getDest().equals(c.getDest())) {
return false;
}
- SubmitType submitType = getSubmitType(object);
- if (submitType == null) {
+ SubmitTypeRecord str = object.submitTypeRecord();
+ if (!str.isOk()) {
return false;
}
ObjectId other = ObjectId.fromString(
object.currentPatchSet().getRevision().get());
ConflictKey conflictsKey =
- new ConflictKey(changeDataCache.getTestAgainst(), other, submitType,
+ new ConflictKey(changeDataCache.getTestAgainst(), other, str.type,
changeDataCache.getProjectState().isUseContentMerge());
Boolean conflicts = args.conflictsCache.getIfPresent(conflictsKey);
if (conflicts != null) {
@@ -117,16 +112,10 @@
try (Repository repo =
args.repoManager.openRepository(otherChange.getProject());
CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
- RevFlag canMergeFlag = rw.newFlag("CAN_MERGE");
- CodeReviewCommit commit =
- rw.parseCommit(changeDataCache.getTestAgainst());
- SubmitStrategy strategy = args.submitStrategyFactory.create(
- submitType, db.get(), repo, rw, null, canMergeFlag,
- getAlreadyAccepted(repo, rw, commit), otherChange.getDest(),
- null);
- CodeReviewCommit otherCommit = rw.parseCommit(other);
- otherCommit.add(canMergeFlag);
- conflicts = !strategy.dryRun(commit, otherCommit);
+ conflicts = !args.submitDryRun.run(
+ str.type, repo, rw, otherChange.getDest(),
+ changeDataCache.getTestAgainst(), other,
+ getAlreadyAccepted(repo, rw));
args.conflictsCache.put(conflictsKey, conflicts);
return conflicts;
} catch (IntegrationException | NoSuchProjectException
@@ -140,36 +129,21 @@
return 5;
}
- private SubmitType getSubmitType(ChangeData cd) throws OrmException {
- SubmitTypeRecord r = new SubmitRuleEvaluator(cd).getSubmitType();
- if (r.status != SubmitTypeRecord.Status.OK) {
- return null;
- }
- return r.type;
- }
-
- private Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw,
- CodeReviewCommit tip) throws IntegrationException {
- Set<RevCommit> alreadyAccepted = Sets.newHashSet();
-
- if (tip != null) {
- alreadyAccepted.add(tip);
- }
-
+ private Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw)
+ throws IntegrationException {
try {
- for (ObjectId id : changeDataCache.getAlreadyAccepted(repo)) {
- try {
- alreadyAccepted.add(rw.parseCommit(id));
- } catch (IncorrectObjectTypeException iote) {
- // Not a commit? Skip over it.
- }
+ Set<RevCommit> accepted = new HashSet<>();
+ SubmitDryRun.addCommits(
+ changeDataCache.getAlreadyAccepted(repo), rw, accepted);
+ ObjectId tip = changeDataCache.getTestAgainst();
+ if (tip != null) {
+ accepted.add(rw.parseCommit(tip));
}
- } catch (IOException e) {
+ return accepted;
+ } catch (OrmException | IOException e) {
throw new IntegrationException(
"Failed to determine already accepted commits.", e);
}
-
- return alreadyAccepted;
}
});
changePredicates.add(and(predicatesForOneChange));
@@ -226,7 +200,7 @@
private ObjectId testAgainst;
private ProjectState projectState;
- private Set<ObjectId> alreadyAccepted;
+ private Iterable<ObjectId> alreadyAccepted;
ChangeDataCache(Change change, Provider<ReviewDb> db,
ChangeData.Factory changeDataFactory, ProjectCache projectCache) {
@@ -257,17 +231,9 @@
return projectState;
}
- Set<ObjectId> getAlreadyAccepted(Repository repo) {
+ Iterable<ObjectId> getAlreadyAccepted(Repository repo) throws IOException {
if (alreadyAccepted == null) {
- alreadyAccepted = Sets.newHashSet();
- for (Ref r : repo.getAllRefs().values()) {
- if (r.getName().startsWith(Constants.R_HEADS)
- || r.getName().startsWith(Constants.R_TAGS)) {
- if (r.getObjectId() != null) {
- alreadyAccepted.add(r.getObjectId());
- }
- }
- }
+ alreadyAccepted = SubmitDryRun.getAlreadyAccepted(repo);
}
return alreadyAccepted;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
index 6a9d86b..c2a9084 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
@@ -17,44 +17,21 @@
import static com.google.gerrit.server.index.ChangeField.EXACT_TOPIC;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.ChangeField;
-import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.IndexPredicate;
-import com.google.gerrit.server.index.Schema;
import com.google.gwtorm.server.OrmException;
class ExactTopicPredicate extends IndexPredicate<ChangeData> {
- @SuppressWarnings("deprecation")
- static FieldDef<ChangeData, ?> topicField(Schema<ChangeData> schema) {
- if (schema == null) {
- return ChangeField.LEGACY_TOPIC2;
- }
- if (schema.hasField(EXACT_TOPIC)) {
- return schema.getFields().get(EXACT_TOPIC.getName());
- }
- if (schema.hasField(ChangeField.LEGACY_TOPIC2)) {
- return schema.getFields().get(ChangeField.LEGACY_TOPIC2.getName());
- }
- // Not exact, but we cannot do any better.
- return schema.getFields().get(ChangeField.LEGACY_TOPIC3.getName());
+ ExactTopicPredicate(String topic) {
+ super(EXACT_TOPIC, topic);
}
- ExactTopicPredicate(Schema<ChangeData> schema, String topic) {
- super(topicField(schema), topic);
- }
-
- @SuppressWarnings("deprecation")
@Override
public boolean match(final ChangeData object) throws OrmException {
Change change = object.change();
if (change == null) {
return false;
}
- String t = change.getTopic();
- if (t == null && getField() == ChangeField.LEGACY_TOPIC2) {
- t = "";
- }
- return getValue().equals(t);
+ return getValue().equals(change.getTopic());
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
index 5b9b94c..154a659 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
@@ -15,15 +15,11 @@
package com.google.gerrit.server.query.change;
import static com.google.gerrit.server.index.ChangeField.FUZZY_TOPIC;
-import static com.google.gerrit.server.index.ChangeField.LEGACY_TOPIC2;
-import static com.google.gerrit.server.index.ChangeField.LEGACY_TOPIC3;
import com.google.common.collect.Iterables;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.index.ChangeIndex;
-import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.IndexPredicate;
-import com.google.gerrit.server.index.Schema;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gwtorm.server.OrmException;
@@ -31,18 +27,11 @@
class FuzzyTopicPredicate extends IndexPredicate<ChangeData> {
private final ChangeIndex index;
- @SuppressWarnings("deprecation")
- static FieldDef<ChangeData, ?> topicField(Schema<ChangeData> schema) {
- return schema.getField(FUZZY_TOPIC, LEGACY_TOPIC3, LEGACY_TOPIC2).get();
- }
-
- FuzzyTopicPredicate(Schema<ChangeData> schema, String topic,
- ChangeIndex index) {
- super(topicField(schema), topic);
+ FuzzyTopicPredicate(String topic, ChangeIndex index) {
+ super(FUZZY_TOPIC, topic);
this.index = index;
}
- @SuppressWarnings("deprecation")
@Override
public boolean match(final ChangeData cd) throws OrmException {
Change change = cd.change();
@@ -53,21 +42,14 @@
if (t == null) {
return false;
}
- if (getField() == FUZZY_TOPIC || getField() == LEGACY_TOPIC3) {
- try {
- Predicate<ChangeData> thisId =
- new LegacyChangeIdPredicate(index.getSchema(), cd.getId());
- Iterable<ChangeData> results =
- index.getSource(and(thisId, this), QueryOptions.oneResult()).read();
- return !Iterables.isEmpty(results);
- } catch (QueryParseException e) {
- throw new OrmException(e);
- }
+ try {
+ Predicate<ChangeData> thisId = new LegacyChangeIdPredicate(cd.getId());
+ Iterable<ChangeData> results =
+ index.getSource(and(thisId, this), QueryOptions.oneResult()).read();
+ return !Iterables.isEmpty(results);
+ } catch (QueryParseException e) {
+ throw new OrmException(e);
}
- if (getField() == LEGACY_TOPIC2) {
- return t.equals(getValue());
- }
- return false;
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByLegacyPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByLegacyPredicate.java
new file mode 100644
index 0000000..578a9f7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByLegacyPredicate.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.server.query.OperatorPredicate;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@Deprecated
+class HasDraftByLegacyPredicate extends OperatorPredicate<ChangeData> implements
+ ChangeDataSource {
+ private final Arguments args;
+ private final Account.Id accountId;
+
+ HasDraftByLegacyPredicate(Arguments args,
+ Account.Id accountId) {
+ super(ChangeQueryBuilder.FIELD_DRAFTBY, accountId.toString());
+ this.args = args;
+ this.accountId = accountId;
+ }
+
+ @Override
+ public boolean match(final ChangeData object) throws OrmException {
+ return !args.plcUtil
+ .draftByChangeAuthor(args.db.get(), object.notes(), accountId)
+ .isEmpty();
+ }
+
+ @Override
+ public ResultSet<ChangeData> read() throws OrmException {
+ Set<Change.Id> ids = new HashSet<>();
+ for (PatchLineComment sc :
+ args.plcUtil.draftByAuthor(args.db.get(), accountId)) {
+ ids.add(sc.getKey().getParentKey().getParentKey().getParentKey());
+ }
+
+ List<ChangeData> r = new ArrayList<>(ids.size());
+ for (Change.Id id : ids) {
+ r.add(args.changeDataFactory.create(args.db.get(), id));
+ }
+ return new ListResultSet<>(r);
+ }
+
+ @Override
+ public boolean hasChange() {
+ return false;
+ }
+
+ @Override
+ public int getCardinality() {
+ return 20;
+ }
+
+ @Override
+ public int getCost() {
+ return 0;
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
index ebb7389..fbc6cfb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
@@ -15,65 +15,25 @@
package com.google.gerrit.server.query.change;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
-import com.google.gwtorm.server.ListResultSet;
+import com.google.gerrit.server.index.ChangeField;
+import com.google.gerrit.server.index.IndexPredicate;
import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-class HasDraftByPredicate extends OperatorPredicate<ChangeData> implements
- ChangeDataSource {
- private final Arguments args;
+class HasDraftByPredicate extends IndexPredicate<ChangeData> {
private final Account.Id accountId;
- HasDraftByPredicate(Arguments args,
- Account.Id accountId) {
- super(ChangeQueryBuilder.FIELD_DRAFTBY, accountId.toString());
- this.args = args;
+ HasDraftByPredicate(Account.Id accountId) {
+ super(ChangeField.DRAFTBY, accountId.toString());
this.accountId = accountId;
}
@Override
- public boolean match(final ChangeData object) throws OrmException {
- return !args.plcUtil
- .draftByChangeAuthor(args.db.get(), object.notes(), accountId)
- .isEmpty();
- }
-
- @Override
- public ResultSet<ChangeData> read() throws OrmException {
- Set<Change.Id> ids = new HashSet<>();
- for (PatchLineComment sc :
- args.plcUtil.draftByAuthor(args.db.get(), accountId)) {
- ids.add(sc.getKey().getParentKey().getParentKey().getParentKey());
- }
-
- List<ChangeData> r = new ArrayList<>(ids.size());
- for (Change.Id id : ids) {
- r.add(args.changeDataFactory.create(args.db.get(), id));
- }
- return new ListResultSet<>(r);
- }
-
- @Override
- public boolean hasChange() {
- return false;
- }
-
- @Override
- public int getCardinality() {
- return 20;
+ public boolean match(ChangeData cd) throws OrmException {
+ return cd.draftsByUser().contains(accountId);
}
@Override
public int getCost() {
- return 0;
+ return 1;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index ff6f2bc..a2b6208 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -214,7 +214,7 @@
public List<ChangeData> byTopicOpen(String topic)
throws OrmException {
- return query(and(new ExactTopicPredicate(schema(indexes), topic), open()));
+ return query(and(new ExactTopicPredicate(topic), open()));
}
public List<ChangeData> byCommit(ObjectId id) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
index 645fa4c..bd8d275 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
@@ -14,18 +14,12 @@
package com.google.gerrit.server.query.change;
-import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.index.ChangeField.LEGACY_REVIEWED;
import static com.google.gerrit.server.index.ChangeField.REVIEWEDBY;
-import com.google.common.base.Optional;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.index.ChangeField;
-import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.IndexPredicate;
-import com.google.gerrit.server.index.Schema;
import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
import com.google.gwtorm.server.OrmException;
import java.util.ArrayList;
@@ -37,20 +31,11 @@
private static final Account.Id NOT_REVIEWED =
new Account.Id(ChangeField.NOT_REVIEWED);
- @SuppressWarnings("deprecation")
- static Predicate<ChangeData> create(Schema<ChangeData> schema) {
- if (getField(schema) == LEGACY_REVIEWED) {
- return new LegacyIsReviewedPredicate();
- }
+ static Predicate<ChangeData> create() {
return Predicate.not(new IsReviewedPredicate(NOT_REVIEWED));
}
- @SuppressWarnings("deprecation")
- static Predicate<ChangeData> create(Schema<ChangeData> schema,
- Collection<Account.Id> ids) throws QueryParseException {
- if (getField(schema) == LEGACY_REVIEWED) {
- throw new QueryParseException("Only is:reviewed is supported");
- }
+ static Predicate<ChangeData> create(Collection<Account.Id> ids) {
List<Predicate<ChangeData>> predicates = new ArrayList<>(ids.size());
for (Account.Id id : ids) {
predicates.add(new IsReviewedPredicate(id));
@@ -58,15 +43,6 @@
return Predicate.or(predicates);
}
- @SuppressWarnings("deprecation")
- private static FieldDef<ChangeData, ?> getField(Schema<ChangeData> schema) {
- Optional<FieldDef<ChangeData, ?>> f =
- schema.getField(REVIEWEDBY, LEGACY_REVIEWED);
- checkState(f.isPresent(), "Schema %s missing field %s",
- schema.getVersion(), REVIEWEDBY.getName());
- return f.get();
- }
-
private final Account.Id id;
private IsReviewedPredicate(Account.Id id) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
index 1ac2729..a3194a1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
@@ -18,7 +18,6 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.index.Schema;
import com.google.gerrit.server.query.OrPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
@@ -38,11 +37,10 @@
return user.toString();
}
- private static List<Predicate<ChangeData>> predicates(
- Schema<ChangeData> schema, Set<Change.Id> ids) {
+ private static List<Predicate<ChangeData>> predicates(Set<Change.Id> ids) {
List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(ids.size());
for (Change.Id id : ids) {
- r.add(new LegacyChangeIdPredicate(schema, id));
+ r.add(new LegacyChangeIdPredicate(id));
}
return r;
}
@@ -55,7 +53,7 @@
}
private IsStarredByPredicate(Arguments args, IdentifiedUser user) {
- super(predicates(args.getSchema(), user.getStarredChanges()));
+ super(predicates(user.getStarredChanges()));
this.args = args;
this.user = user;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
index bf59553..f4a1550 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -15,31 +15,16 @@
package com.google.gerrit.server.query.change;
import static com.google.gerrit.server.index.ChangeField.LEGACY_ID;
-import static com.google.gerrit.server.index.ChangeField.LEGACY_ID2;
import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.index.ChangeField;
-import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.IndexPredicate;
-import com.google.gerrit.server.index.Schema;
/** Predicate over change number (aka legacy ID or Change.Id). */
public class LegacyChangeIdPredicate extends IndexPredicate<ChangeData> {
private final Change.Id id;
- @SuppressWarnings("deprecation")
- public static FieldDef<ChangeData, ?> idField(Schema<ChangeData> schema) {
- if (schema == null) {
- return ChangeField.LEGACY_ID2;
- } else if (schema.hasField(LEGACY_ID2)) {
- return schema.getFields().get(LEGACY_ID2.getName());
- } else {
- return schema.getFields().get(LEGACY_ID.getName());
- }
- }
-
- LegacyChangeIdPredicate(Schema<ChangeData> schema, Change.Id id) {
- super(idField(schema), ChangeQueryBuilder.FIELD_CHANGE, id.toString());
+ LegacyChangeIdPredicate(Change.Id id) {
+ super(LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
this.id = id;
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyIsReviewedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyIsReviewedPredicate.java
deleted file mode 100644
index e12e6e0..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyIsReviewedPredicate.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.server.index.ChangeField;
-import com.google.gerrit.server.index.IndexPredicate;
-import com.google.gwtorm.server.OrmException;
-
-@Deprecated
-class LegacyIsReviewedPredicate extends IndexPredicate<ChangeData> {
- @Deprecated
- LegacyIsReviewedPredicate() {
- super(ChangeField.LEGACY_REVIEWED, "1");
- }
-
- @Override
- public boolean match(final ChangeData object) throws OrmException {
- Change c = object.change();
- if (c == null) {
- return false;
- }
-
- PatchSet.Id current = c.currentPatchSetId();
- for (PatchSetApproval p : object.approvals().get(current)) {
- if (p.getValue() != 0) {
- return true;
- }
- }
-
- return false;
- }
-
- @Override
- public int getCost() {
- return 2;
- }
-
- @Override
- public String toString() {
- return "is:reviewed";
- }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
index cf3140a..43d7708 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -37,8 +37,7 @@
public boolean match(ChangeData object) throws OrmException {
try {
Predicate<ChangeData> p = Predicate.and(
- new LegacyChangeIdPredicate(index.getSchema(), object.getId()),
- this);
+ new LegacyChangeIdPredicate(object.getId()), this);
for (ChangeData cData
: index.getSource(p, QueryOptions.oneResult()).read()) {
if (cData.getId().equals(object.getId())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
index de7005f..35b94ea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -14,9 +14,10 @@
package com.google.gerrit.server.query.change;
+import static com.google.gerrit.server.index.ChangeField.FUZZY_TOPIC;
+
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.index.RegexPredicate;
-import com.google.gerrit.server.index.Schema;
import com.google.gwtorm.server.OrmException;
import dk.brics.automaton.RegExp;
@@ -25,8 +26,8 @@
class RegexTopicPredicate extends RegexPredicate<ChangeData> {
private final RunAutomaton pattern;
- RegexTopicPredicate(Schema<ChangeData> schema, String re) {
- super(FuzzyTopicPredicate.topicField(schema), re);
+ RegexTopicPredicate(String re) {
+ super(FUZZY_TOPIC, re);
if (re.startsWith("^")) {
re = re.substring(1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
index 9b6e36a..e6d9385 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -114,68 +114,69 @@
private void initAllProjects(Repository git)
throws IOException, ConfigInvalidException {
- MetaDataUpdate md = new MetaDataUpdate(
- GitReferenceUpdated.DISABLED,
- allProjectsName,
- git);
- md.getCommitBuilder().setAuthor(serverUser);
- md.getCommitBuilder().setCommitter(serverUser);
- md.setMessage(MoreObjects.firstNonNull(
- Strings.emptyToNull(message),
- "Initialized Gerrit Code Review " + Version.getVersion()));
+ try (MetaDataUpdate md = new MetaDataUpdate(
+ GitReferenceUpdated.DISABLED,
+ allProjectsName,
+ git)) {
+ md.getCommitBuilder().setAuthor(serverUser);
+ md.getCommitBuilder().setCommitter(serverUser);
+ md.setMessage(MoreObjects.firstNonNull(
+ Strings.emptyToNull(message),
+ "Initialized Gerrit Code Review " + Version.getVersion()));
- ProjectConfig config = ProjectConfig.read(md);
- Project p = config.getProject();
- p.setDescription("Access inherited by all other projects.");
- p.setRequireChangeID(InheritableBoolean.TRUE);
- p.setUseContentMerge(InheritableBoolean.TRUE);
- p.setUseContributorAgreements(InheritableBoolean.FALSE);
- p.setUseSignedOffBy(InheritableBoolean.FALSE);
- p.setEnableSignedPush(InheritableBoolean.FALSE);
+ ProjectConfig config = ProjectConfig.read(md);
+ Project p = config.getProject();
+ p.setDescription("Access inherited by all other projects.");
+ p.setRequireChangeID(InheritableBoolean.TRUE);
+ p.setUseContentMerge(InheritableBoolean.TRUE);
+ p.setUseContributorAgreements(InheritableBoolean.FALSE);
+ p.setUseSignedOffBy(InheritableBoolean.FALSE);
+ p.setEnableSignedPush(InheritableBoolean.FALSE);
- AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
- AccessSection all = config.getAccessSection(AccessSection.ALL, true);
- AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
- AccessSection tags = config.getAccessSection("refs/tags/*", true);
- AccessSection meta = config.getAccessSection(RefNames.REFS_CONFIG, true);
- AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
+ AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
+ AccessSection all = config.getAccessSection(AccessSection.ALL, true);
+ AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
+ AccessSection tags = config.getAccessSection("refs/tags/*", true);
+ AccessSection meta = config.getAccessSection(RefNames.REFS_CONFIG, true);
+ AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
- grant(config, cap, GlobalCapability.ADMINISTRATE_SERVER, admin);
- grant(config, all, Permission.READ, admin, anonymous);
+ grant(config, cap, GlobalCapability.ADMINISTRATE_SERVER, admin);
+ grant(config, all, Permission.READ, admin, anonymous);
- if (batch != null) {
- Permission priority = cap.getPermission(GlobalCapability.PRIORITY, true);
- PermissionRule r = rule(config, batch);
- r.setAction(Action.BATCH);
- priority.add(r);
+ if (batch != null) {
+ Permission priority = cap.getPermission(GlobalCapability.PRIORITY, true);
+ PermissionRule r = rule(config, batch);
+ r.setAction(Action.BATCH);
+ priority.add(r);
- Permission stream = cap.getPermission(GlobalCapability.STREAM_EVENTS, true);
- stream.add(rule(config, batch));
+ Permission stream = cap.getPermission(GlobalCapability.STREAM_EVENTS, true);
+ stream.add(rule(config, batch));
+ }
+
+ LabelType cr = initCodeReviewLabel(config);
+ grant(config, heads, cr, -1, 1, registered);
+ grant(config, heads, cr, -2, 2, admin, owners);
+ grant(config, heads, Permission.CREATE, admin, owners);
+ grant(config, heads, Permission.PUSH, admin, owners);
+ grant(config, heads, Permission.SUBMIT, admin, owners);
+ grant(config, heads, Permission.FORGE_AUTHOR, registered);
+ grant(config, heads, Permission.FORGE_COMMITTER, admin, owners);
+ grant(config, heads, Permission.EDIT_TOPIC_NAME, true, admin, owners);
+
+ grant(config, tags, Permission.PUSH_TAG, admin, owners);
+ grant(config, tags, Permission.PUSH_SIGNED_TAG, admin, owners);
+
+ grant(config, magic, Permission.PUSH, registered);
+ grant(config, magic, Permission.PUSH_MERGE, registered);
+
+ meta.getPermission(Permission.READ, true).setExclusiveGroup(true);
+ grant(config, meta, Permission.READ, admin, owners);
+ grant(config, meta, cr, -2, 2, admin, owners);
+ grant(config, meta, Permission.PUSH, admin, owners);
+ grant(config, meta, Permission.SUBMIT, admin, owners);
+
+ config.commitToNewRef(md, RefNames.REFS_CONFIG);
}
-
- LabelType cr = initCodeReviewLabel(config);
- grant(config, heads, cr, -1, 1, registered);
- grant(config, heads, cr, -2, 2, admin, owners);
- grant(config, heads, Permission.CREATE, admin, owners);
- grant(config, heads, Permission.PUSH, admin, owners);
- grant(config, heads, Permission.SUBMIT, admin, owners);
- grant(config, heads, Permission.FORGE_AUTHOR, registered);
- grant(config, heads, Permission.FORGE_COMMITTER, admin, owners);
- grant(config, heads, Permission.EDIT_TOPIC_NAME, true, admin, owners);
-
- grant(config, tags, Permission.PUSH_TAG, admin, owners);
- grant(config, tags, Permission.PUSH_SIGNED_TAG, admin, owners);
-
- grant(config, magic, Permission.PUSH, registered);
- grant(config, magic, Permission.PUSH_MERGE, registered);
-
- meta.getPermission(Permission.READ, true).setExclusiveGroup(true);
- grant(config, meta, Permission.READ, admin, owners);
- grant(config, meta, cr, -2, 2, admin, owners);
- grant(config, meta, Permission.PUSH, admin, owners);
- grant(config, meta, Permission.SUBMIT, admin, owners);
-
- config.commitToNewRef(md, RefNames.REFS_CONFIG);
}
public static LabelType initCodeReviewLabel(ProjectConfig c) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java
index 22a345a..79849fb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllUsersCreator.java
@@ -75,29 +75,30 @@
private void initAllUsers(Repository git)
throws IOException, ConfigInvalidException {
- MetaDataUpdate md = new MetaDataUpdate(
- GitReferenceUpdated.DISABLED,
- allUsersName,
- git);
- md.getCommitBuilder().setAuthor(serverUser);
- md.getCommitBuilder().setCommitter(serverUser);
- md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
+ try (MetaDataUpdate md = new MetaDataUpdate(
+ GitReferenceUpdated.DISABLED,
+ allUsersName,
+ git)) {
+ md.getCommitBuilder().setAuthor(serverUser);
+ md.getCommitBuilder().setCommitter(serverUser);
+ md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
- ProjectConfig config = ProjectConfig.read(md);
- Project project = config.getProject();
- project.setDescription("Individual user settings and preferences.");
+ ProjectConfig config = ProjectConfig.read(md);
+ Project project = config.getProject();
+ project.setDescription("Individual user settings and preferences.");
- AccessSection all = config.getAccessSection(RefNames.REFS_USERS + "*", true);
- all.getPermission(Permission.READ, true).setExclusiveGroup(true);
+ AccessSection all = config.getAccessSection(RefNames.REFS_USERS + "*", true);
+ all.getPermission(Permission.READ, true).setExclusiveGroup(true);
- AccessSection defaults = config.getAccessSection(RefNames.REFS_USERS_DEFAULT, true);
- defaults.getPermission(Permission.READ, true).setExclusiveGroup(true);
- grant(config, defaults, Permission.READ, admin);
- defaults.getPermission(Permission.PUSH, true).setExclusiveGroup(true);
- grant(config, defaults, Permission.PUSH, admin);
- defaults.getPermission(Permission.CREATE, true).setExclusiveGroup(true);
- grant(config, defaults, Permission.CREATE, admin);
+ AccessSection defaults = config.getAccessSection(RefNames.REFS_USERS_DEFAULT, true);
+ defaults.getPermission(Permission.READ, true).setExclusiveGroup(true);
+ grant(config, defaults, Permission.READ, admin);
+ defaults.getPermission(Permission.PUSH, true).setExclusiveGroup(true);
+ grant(config, defaults, Permission.PUSH, admin);
+ defaults.getPermission(Permission.CREATE, true).setExclusiveGroup(true);
+ grant(config, defaults, Permission.CREATE, admin);
- config.commit(md);
+ config.commit(md);
+ }
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java
index 4e76325..12a70eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_108.java
@@ -33,6 +33,7 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
+import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
@@ -72,7 +73,7 @@
: openByProject.asMap().entrySet()) {
try (Repository repo = repoManager.openRepository(e.getKey());
RevWalk rw = new RevWalk(repo)) {
- updateProjectGroups(db, repo, rw, (Set<Change.Id>) e.getValue());
+ updateProjectGroups(db, repo, rw, (Set<Change.Id>) e.getValue(), ui);
} catch (IOException err) {
throw new OrmException(err);
}
@@ -84,7 +85,8 @@
}
private static void updateProjectGroups(ReviewDb db, Repository repo,
- RevWalk rw, Set<Change.Id> changes) throws OrmException, IOException {
+ RevWalk rw, Set<Change.Id> changes, UpdateUI ui)
+ throws OrmException, IOException {
// Match sorting in ReceiveCommits.
rw.reset();
rw.sort(RevSort.TOPO);
@@ -92,7 +94,7 @@
RefDatabase refdb = repo.getRefDatabase();
for (Ref ref : refdb.getRefs(Constants.R_HEADS).values()) {
- RevCommit c = maybeParseCommit(rw, ref.getObjectId());
+ RevCommit c = maybeParseCommit(rw, ref.getObjectId(), ui);
if (c != null) {
rw.markUninteresting(c);
}
@@ -110,7 +112,7 @@
PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
if (psId != null && changes.contains(psId.getParentKey())) {
patchSetsBySha.put(id, psId);
- RevCommit c = maybeParseCommit(rw, id);
+ RevCommit c = maybeParseCommit(rw, id, ui);
if (c != null) {
rw.markStart(c);
}
@@ -175,12 +177,16 @@
return openByProject;
}
- private static RevCommit maybeParseCommit(RevWalk rw, ObjectId id)
+ private static RevCommit maybeParseCommit(RevWalk rw, ObjectId id, UpdateUI ui)
throws IOException {
- if (id == null) {
- return null;
+ if (id != null) {
+ try {
+ RevObject obj = rw.parseAny(id);
+ return (obj instanceof RevCommit) ? (RevCommit) obj : null;
+ } catch (MissingObjectException moe) {
+ ui.message("Missing object: " + id.getName() + "\n");
+ }
}
- RevObject obj = rw.parseAny(id);
- return (obj instanceof RevCommit) ? (RevCommit) obj : null;
+ return null;
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_115.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_115.java
index 920160b..30479b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_115.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_115.java
@@ -131,18 +131,18 @@
try (Repository git = mgr.openRepository(allUsersName);
RevWalk rw = new RevWalk(git)) {
BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
- MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
- allUsersName, git, bru);
- md.getCommitBuilder().setAuthor(serverUser);
- md.getCommitBuilder().setCommitter(serverUser);
-
for (Map.Entry<Account.Id, DiffPreferencesInfo> e : imports.entrySet()) {
- VersionedAccountPreferences p =
- VersionedAccountPreferences.forUser(e.getKey());
- p.load(md);
- storeSection(p.getConfig(), UserConfigSections.DIFF, null,
- e.getValue(), DiffPreferencesInfo.defaults());
- p.commit(md);
+ try(MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED,
+ allUsersName, git, bru)) {
+ md.getCommitBuilder().setAuthor(serverUser);
+ md.getCommitBuilder().setCommitter(serverUser);
+ VersionedAccountPreferences p =
+ VersionedAccountPreferences.forUser(e.getKey());
+ p.load(md);
+ storeSection(p.getConfig(), UserConfigSections.DIFF, null,
+ e.getValue(), DiffPreferencesInfo.defaults());
+ p.commit(md);
+ }
}
bru.execute(rw, NullProgressMonitor.INSTANCE);
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index 5370156..aace2b3 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -19,7 +19,7 @@
unset GREP_OPTIONS
-CHANGE_ID_AFTER="Bug|Issue"
+CHANGE_ID_AFTER="Bug|Issue|Test"
MSG="$1"
# Check for, and add if missing, a unique Change-Id
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
index b3f7894..23ec9a9 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/LabelNormalizerTest.java
@@ -196,10 +196,11 @@
}
private void save(ProjectConfig pc) throws Exception {
- MetaDataUpdate md =
- metaDataUpdateFactory.create(pc.getProject().getNameKey(), user);
- pc.commit(md);
- projectCache.evict(pc.getProject().getNameKey());
+ try(MetaDataUpdate md =
+ metaDataUpdateFactory.create(pc.getProject().getNameKey(), user)) {
+ pc.commit(md);
+ projectCache.evict(pc.getProject().getNameKey());
+ }
}
private PatchSetApproval psa(Account.Id accountId, String label, int value) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
index 4f32ba8..fcc7288 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -249,17 +249,18 @@
private RevCommit commit(ProjectConfig cfg) throws IOException,
MissingObjectException, IncorrectObjectTypeException {
- MetaDataUpdate md = new MetaDataUpdate(
- GitReferenceUpdated.DISABLED,
- cfg.getProject().getNameKey(),
- db);
- util.tick(5);
- util.setAuthorAndCommitter(md.getCommitBuilder());
- md.setMessage("Edit\n");
- cfg.commit(md);
+ try (MetaDataUpdate md = new MetaDataUpdate(
+ GitReferenceUpdated.DISABLED,
+ cfg.getProject().getNameKey(),
+ db)) {
+ util.tick(5);
+ util.setAuthorAndCommitter(md.getCommitBuilder());
+ md.setMessage("Edit\n");
+ cfg.commit(md);
- Ref ref = db.getRef(RefNames.REFS_CONFIG);
- return util.getRevWalk().parseCommit(ref.getObjectId());
+ Ref ref = db.getRef(RefNames.REFS_CONFIG);
+ return util.getRevWalk().parseCommit(ref.getObjectId());
+ }
}
private void update(RevCommit rev) throws Exception {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 0f3fee4..5af78461 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -112,6 +112,11 @@
+ "Label: Label2=1\n"
+ "Label: Label3=0\n"
+ "Label: Label4=-1\n");
+ assertParseSucceeds("Update change\n"
+ + "\n"
+ + "Patch-Set: 1\n"
+ + "Label: -Label1\n"
+ + "Label: -Label1 Other Account <2@gerrit>\n");
assertParseFails("Update change\n"
+ "\n"
+ "Patch-Set: 1\n"
@@ -124,6 +129,18 @@
+ "\n"
+ "Patch-Set: 1\n"
+ "Label: X+Y\n");
+ assertParseFails("Update change\n"
+ + "\n"
+ + "Patch-Set: 1\n"
+ + "Label: Label1 Other Account <2@gerrit>\n");
+ assertParseFails("Update change\n"
+ + "\n"
+ + "Patch-Set: 1\n"
+ + "Label: -Label!1\n");
+ assertParseFails("Update change\n"
+ + "\n"
+ + "Patch-Set: 1\n"
+ + "Label: -Label!1 Other Account <2@gerrit>\n");
}
@Test
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 c68c283..04b51fd 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
@@ -197,6 +197,41 @@
}
@Test
+ public void removeOtherUsersApprovals() throws Exception {
+ Change c = newChange();
+ ChangeUpdate update = newUpdate(c, otherUser);
+ update.putApproval("Not-For-Long", (short) 1);
+ update.commit();
+
+ ChangeNotes notes = newNotes(c);
+ PatchSetApproval psa = Iterables.getOnlyElement(
+ notes.getApprovals().get(c.currentPatchSetId()));
+ assertThat(psa.getAccountId()).isEqualTo(otherUserId);
+ assertThat(psa.getLabel()).isEqualTo("Not-For-Long");
+ assertThat(psa.getValue()).isEqualTo((short) 1);
+
+ update = newUpdate(c, changeOwner);
+ update.removeApprovalFor(otherUserId, "Not-For-Long");
+ update.commit();
+
+ notes = newNotes(c);
+ assertThat(notes.getApprovals()).isEmpty();
+
+ // Add back approval on same label.
+ update = newUpdate(c, otherUser);
+ update.putApproval("Not-For-Long", (short) 2);
+ update.commit();
+
+ notes = newNotes(c);
+ psa = Iterables.getOnlyElement(
+ notes.getApprovals().get(c.currentPatchSetId()));
+ assertThat(psa.getAccountId()).isEqualTo(otherUserId);
+ assertThat(psa.getLabel()).isEqualTo("Not-For-Long");
+ assertThat(psa.getValue()).isEqualTo((short) 2);
+
+ }
+
+ @Test
public void multipleReviewers() throws Exception {
Change c = newChange();
ChangeUpdate update = newUpdate(c, changeOwner);
@@ -373,7 +408,6 @@
@Test
public void topicChangeNotes() throws Exception {
Change c = newChange();
- ChangeUpdate update = newUpdate(c, changeOwner);
// initially topic is not set
ChangeNotes notes = newNotes(c);
@@ -382,12 +416,14 @@
// set topic
String topic = "myTopic";
+ ChangeUpdate update = newUpdate(c, changeOwner);
update.setTopic(topic);
update.commit();
notes = newNotes(c);
assertThat(notes.getChange().getTopic()).isEqualTo(topic);
// clear topic by setting empty string
+ update = newUpdate(c, changeOwner);
update.setTopic("");
update.commit();
notes = newNotes(c);
@@ -395,12 +431,14 @@
// set other topic
topic = "otherTopic";
+ update = newUpdate(c, changeOwner);
update.setTopic(topic);
update.commit();
notes = newNotes(c);
assertThat(notes.getChange().getTopic()).isEqualTo(topic);
// clear topic by setting null
+ update = newUpdate(c, changeOwner);
update.setTopic(null);
update.commit();
notes = newNotes(c);
@@ -869,7 +907,7 @@
update.upsertComment(commentForPS);
update.commit();
- assertThat(newNotes(c).getComments()).containsExactly(
+ assertThat(newNotes(c).getComments()).containsExactlyEntriesIn(
ImmutableMultimap.of(
new RevId(rev1), commentForBase,
new RevId(rev2), commentForPS));
@@ -904,7 +942,7 @@
update.upsertComment(comment2);
update.commit();
- assertThat(newNotes(c).getComments()).containsExactly(
+ assertThat(newNotes(c).getComments()).containsExactlyEntriesIn(
ImmutableMultimap.of(
new RevId(rev), comment1,
new RevId(rev), comment2)).inOrder();
@@ -939,7 +977,7 @@
update.upsertComment(comment2);
update.commit();
- assertThat(newNotes(c).getComments()).containsExactly(
+ assertThat(newNotes(c).getComments()).containsExactlyEntriesIn(
ImmutableMultimap.of(
new RevId(rev), comment1,
new RevId(rev), comment2)).inOrder();
@@ -977,7 +1015,7 @@
update.upsertComment(comment2);
update.commit();
- assertThat(newNotes(c).getComments()).containsExactly(
+ assertThat(newNotes(c).getComments()).containsExactlyEntriesIn(
ImmutableMultimap.of(
new RevId(rev1), comment1,
new RevId(rev2), comment2));
@@ -1003,7 +1041,7 @@
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getDraftComments(otherUserId)).containsExactly(
+ assertThat(notes.getDraftComments(otherUserId)).containsExactlyEntriesIn(
ImmutableMultimap.of(new RevId(rev), comment1));
assertThat(notes.getComments()).isEmpty();
@@ -1015,7 +1053,7 @@
notes = newNotes(c);
assertThat(notes.getDraftComments(otherUserId)).isEmpty();
- assertThat(notes.getComments()).containsExactly(
+ assertThat(notes.getComments()).containsExactlyEntriesIn(
ImmutableMultimap.of(new RevId(rev), comment1));
}
@@ -1047,7 +1085,7 @@
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getDraftComments(otherUserId)).containsExactly(
+ assertThat(notes.getDraftComments(otherUserId)).containsExactlyEntriesIn(
ImmutableMultimap.of(
new RevId(rev), comment1,
new RevId(rev), comment2)).inOrder();
@@ -1061,9 +1099,9 @@
update.commit();
notes = newNotes(c);
- assertThat(notes.getDraftComments(otherUserId)).containsExactly(
+ assertThat(notes.getDraftComments(otherUserId)).containsExactlyEntriesIn(
ImmutableMultimap.of(new RevId(rev), comment2));
- assertThat(notes.getComments()).containsExactly(
+ assertThat(notes.getComments()).containsExactlyEntriesIn(
ImmutableMultimap.of(new RevId(rev), comment1));
}
@@ -1096,7 +1134,7 @@
update.commit();
ChangeNotes notes = newNotes(c);
- assertThat(notes.getDraftComments(otherUserId)).containsExactly(
+ assertThat(notes.getDraftComments(otherUserId)).containsExactlyEntriesIn(
ImmutableMultimap.of(
new RevId(rev1), baseComment,
new RevId(rev2), psComment));
@@ -1114,7 +1152,7 @@
notes = newNotes(c);
assertThat(notes.getDraftComments(otherUserId)).isEmpty();
- assertThat(notes.getComments()).containsExactly(
+ assertThat(notes.getComments()).containsExactlyEntriesIn(
ImmutableMultimap.of(
new RevId(rev1), baseComment,
new RevId(rev2), psComment));
@@ -1224,7 +1262,7 @@
update.upsertComment(comment);
update.commit();
- assertThat(newNotes(c).getComments()).containsExactly(
+ assertThat(newNotes(c).getComments()).containsExactlyEntriesIn(
ImmutableMultimap.of(new RevId(rev), comment));
}
@@ -1245,7 +1283,7 @@
update.upsertComment(comment);
update.commit();
- assertThat(newNotes(c).getComments()).containsExactly(
+ assertThat(newNotes(c).getComments()).containsExactlyEntriesIn(
ImmutableMultimap.of(new RevId(rev), comment));
}
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 06bfafc..aa42fc7 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
@@ -35,6 +35,7 @@
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.changes.Changes.QueryRequest;
+import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.HashtagsInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.groups.GroupInput;
@@ -287,6 +288,28 @@
}
@Test
+ public void byStatusDraft() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ ChangeInserter ins1 = newChange(repo, null, null, null, null);
+ Change change1 = ins1.getChange();
+ change1.setStatus(Change.Status.NEW);
+ insert(ins1);
+ ChangeInserter ins2 = newChange(repo, null, null, null, null);
+ Change change2 = ins2.getChange();
+ change2.setStatus(Change.Status.DRAFT);
+ insert(ins2);
+
+ Change[] expected = new Change[] {change2};
+ assertQuery("status:draft", expected);
+ assertQuery("status:DRAFT", expected);
+ assertQuery("status:d", expected);
+ assertQuery("status:dr", expected);
+ assertQuery("status:dra", expected);
+ assertQuery("status:draf", expected);
+ assertQuery("is:draft", expected);
+ }
+
+ @Test
public void byStatusClosed() throws Exception {
TestRepository<Repo> repo = createProject("repo");
ChangeInserter ins1 = newChange(repo, null, null, null, null);
@@ -378,6 +401,10 @@
// By name part
assertQuery("author:Author", change1);
+ // Case insensitive
+ assertQuery("author:jAuThOr", change1);
+ assertQuery("author:ExAmPlE", change1);
+
// By non-existing email address / name / part
assertQuery("author:jcommitter@example.com");
assertQuery("author:somewhere.com");
@@ -401,6 +428,10 @@
// By name part
assertQuery("committer:Committer", change1);
+ // Case insensitive
+ assertQuery("committer:jCoMmItTeR", change1);
+ assertQuery("committer:ExAmPlE", change1);
+
// By non-existing email address / name / part
assertQuery("committer:jauthor@example.com");
assertQuery("committer:somewhere.com");
@@ -1130,6 +1161,31 @@
}
@Test
+ public void byDraftBy() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(newChange(repo, null, null, null, null));
+ Change change2 = insert(newChange(repo, null, null, null, null));
+
+ DraftInput in = new DraftInput();
+ in.line = 1;
+ in.message = "nit: trailing whitespace";
+ in.path = Patch.COMMIT_MSG;
+ gApi.changes().id(change1.getId().get()).current().createDraft(in);
+
+ in = new DraftInput();
+ in.line = 2;
+ in.message = "nit: point in the end of the statement";
+ in.path = Patch.COMMIT_MSG;
+ gApi.changes().id(change2.getId().get()).current().createDraft(in);
+
+ int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
+ .getAccountId().get();
+
+ assertQuery("draftby:" + userId.get(), change2, change1);
+ assertQuery("draftby:" + user2);
+ }
+
+ @Test
public void byFrom() throws Exception {
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(newChange(repo, null, null, null, null));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
deleted file mode 100644
index ed4e520..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesV14Test.java
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright (C) 2015 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import static com.google.common.truth.Truth.assertThat;
-import static java.util.concurrent.TimeUnit.MINUTES;
-
-import com.google.gerrit.extensions.api.changes.ReviewInput;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.testutil.InMemoryModule;
-import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo;
-import com.google.gerrit.testutil.TestTimeUtil;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-import org.eclipse.jgit.junit.TestRepository;
-import org.eclipse.jgit.lib.Config;
-import org.junit.Ignore;
-import org.junit.Test;
-
-public class LuceneQueryChangesV14Test extends LuceneQueryChangesTest {
- @Override
- protected Injector createInjector() {
- Config luceneConfig = new Config(config);
- InMemoryModule.setDefaults(luceneConfig);
- // Latest version with a Lucene 4 index.
- luceneConfig.setInt("index", "lucene", "testVersion", 14);
- return Guice.createInjector(new InMemoryModule(luceneConfig));
- }
-
- @Override
- @Ignore
- @Test
- public void byCommentBy() {
- // Ignore.
- }
-
- @Override
- @Ignore
- @Test
- public void byFrom() {
- // Ignore.
- }
-
- @Override
- @Ignore
- @Test
- public void byTopic() {
- // Ignore.
- }
-
- @Override
- @Ignore
- @Test
- public void reviewedBy() throws Exception {
- // Ignore.
- }
-
- @Override
- @Ignore
- @Test
- public void prepopulatedFields() throws Exception {
- // Ignore.
- }
-
- @Override
- @Ignore
- @Test
- public void prepopulateOnlyRequestedFields() throws Exception {
- // Ignore.
- }
-
- @Test
- public void isReviewed() throws Exception {
- TestTimeUtil.resetWithClockStep(2, MINUTES);
- TestRepository<Repo> repo = createProject("repo");
- Change change1 = insert(newChange(repo, null, null, null, null));
- Change change2 = insert(newChange(repo, null, null, null, null));
- Change change3 = insert(newChange(repo, null, null, null, null));
-
- gApi.changes()
- .id(change1.getId().get())
- .current()
- .review(new ReviewInput().message("comment"));
-
- Account.Id user2 = accountManager
- .authenticate(AuthRequest.forUser("anotheruser"))
- .getAccountId();
- requestContext.setContext(newRequestContext(user2));
-
- gApi.changes()
- .id(change2.getId().get())
- .current()
- .review(ReviewInput.recommend());
-
- PatchSet.Id ps3_1 = change3.currentPatchSetId();
- change3 = newPatchSet(repo, change3);
- assertThat(change3.currentPatchSetId()).isNotEqualTo(ps3_1);
- // Nonzero score on previous patch set does not count.
- gApi.changes()
- .id(change3.getId().get())
- .revision(ps3_1.get())
- .review(ReviewInput.recommend());
-
- assertQuery("is:reviewed", change2);
- assertQuery("-is:reviewed", change3, change1);
- }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java
index 39989a9..4d9f617 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java
@@ -14,6 +14,7 @@
package com.google.gerrit.testutil;
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.server.notedb.NotesMigration;
import org.eclipse.jgit.lib.Config;
@@ -23,7 +24,7 @@
import org.junit.runner.RunWith;
import org.junit.runners.model.Statement;
-import java.util.Arrays;
+import java.util.List;
@RunWith(ConfigSuite.class)
public class GerritServerTests extends GerritBaseTests {
@@ -34,10 +35,9 @@
private String configName;
public static boolean isNoteDbTestEnabled() {
- final String[] RUN_FLAGS = {"yes", "y", "true"};
+ List<String> runValues = ImmutableList.of("yes", "y", "true", "1");
String value = System.getenv("GERRIT_ENABLE_NOTEDB");
- return value != null &&
- Arrays.asList(RUN_FLAGS).contains(value.toLowerCase());
+ return value != null && runValues.contains(value.toLowerCase());
}
@Rule
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index 6ed70a2..2fb91f6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -145,17 +145,12 @@
continue;
}
- try {
- MetaDataUpdate md = metaDataUpdateFactory.create(nameKey);
- try {
- ProjectConfig config = ProjectConfig.read(md);
- config.getProject().setParentName(newParentKey);
- md.setMessage("Inherit access from "
- + (newParentKey != null ? newParentKey.get() : allProjectsName.get()) + "\n");
- config.commit(md);
- } finally {
- md.close();
- }
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(nameKey)) {
+ ProjectConfig config = ProjectConfig.read(md);
+ config.getProject().setParentName(newParentKey);
+ md.setMessage("Inherit access from "
+ + (newParentKey != null ? newParentKey.get() : allProjectsName.get()) + "\n");
+ config.commit(md);
} catch (RepositoryNotFoundException notFound) {
err.append("error: Project ").append(name).append(" not found\n");
} catch (IOException | ConfigInvalidException e) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
index 5eda57c..301bc0e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
@@ -14,6 +14,8 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput.Filters;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -21,8 +23,6 @@
import com.google.gerrit.server.change.ChangesCollection;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.Revisions;
-import com.google.gerrit.server.change.TestSubmitRule.Filters;
-import com.google.gerrit.server.change.TestSubmitRule.Input;
import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject;
@@ -34,7 +34,7 @@
import java.nio.ByteBuffer;
abstract class BaseTestPrologCommand extends SshCommand {
- private Input input = new Input();
+ private TestSubmitRuleInput input = new TestSubmitRuleInput();
@Inject
private ChangesCollection changes;
@@ -55,7 +55,7 @@
input.filters = no ? Filters.SKIP : Filters.RUN;
}
- protected abstract RestModifyView<RevisionResource, Input> createView();
+ protected abstract RestModifyView<RevisionResource, TestSubmitRuleInput> createView();
@Override
protected final void run() throws UnloggedFailure {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 4570085..54e07aa3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -122,6 +122,10 @@
@Option(name = "--json", aliases = "-j", usage = "read review input json from stdin")
private boolean json;
+ @Option(name = "--strict-labels", usage = "Strictly check if the labels "
+ + "specified can be applied to the given patch set(s)")
+ private boolean strictLabels;
+
@Option(name = "--label", aliases = "-l", usage = "custom label(s) to assign", metaVar = "LABEL=VALUE")
void addLabel(final String token) {
LabelVote v = LabelVote.parseWithEquals(token);
@@ -272,7 +276,7 @@
review.notify = notify;
review.labels = Maps.newTreeMap();
review.drafts = ReviewInput.DraftHandling.PUBLISH;
- review.strictLabels = false;
+ review.strictLabels = strictLabels;
for (ApproveOption ao : optionList) {
Short v = ao.value();
if (v != null) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index ab1353b..589fbf0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -124,41 +124,36 @@
String name = ctlProject.getName();
final StringBuilder err = new StringBuilder();
- try {
- MetaDataUpdate md = metaDataUpdateFactory.create(nameKey);
- try {
- ProjectConfig config = ProjectConfig.read(md);
- Project project = config.getProject();
+ try (MetaDataUpdate md = metaDataUpdateFactory.create(nameKey)) {
+ ProjectConfig config = ProjectConfig.read(md);
+ Project project = config.getProject();
- if (requireChangeID != null) {
- project.setRequireChangeID(requireChangeID);
- }
- if (submitType != null) {
- project.setSubmitType(submitType);
- }
- if (contentMerge != null) {
- project.setUseContentMerge(contentMerge);
- }
- if (contributorAgreements != null) {
- project.setUseContributorAgreements(contributorAgreements);
- }
- if (signedOffBy != null) {
- project.setUseSignedOffBy(signedOffBy);
- }
- if (projectDescription != null) {
- project.setDescription(projectDescription);
- }
- if (state != null) {
- project.setState(state);
- }
- if (maxObjectSizeLimit != null) {
- project.setMaxObjectSizeLimit(maxObjectSizeLimit);
- }
- md.setMessage("Project settings updated");
- config.commit(md);
- } finally {
- md.close();
+ if (requireChangeID != null) {
+ project.setRequireChangeID(requireChangeID);
}
+ if (submitType != null) {
+ project.setSubmitType(submitType);
+ }
+ if (contentMerge != null) {
+ project.setUseContentMerge(contentMerge);
+ }
+ if (contributorAgreements != null) {
+ project.setUseContributorAgreements(contributorAgreements);
+ }
+ if (signedOffBy != null) {
+ project.setUseSignedOffBy(signedOffBy);
+ }
+ if (projectDescription != null) {
+ project.setDescription(projectDescription);
+ }
+ if (state != null) {
+ project.setState(state);
+ }
+ if (maxObjectSizeLimit != null) {
+ project.setMaxObjectSizeLimit(maxObjectSizeLimit);
+ }
+ md.setMessage("Project settings updated");
+ config.commit(md);
} catch (RepositoryNotFoundException notFound) {
err.append("error: Project ").append(name).append(" not found\n");
} catch (IOException | ConfigInvalidException e) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
index b957a7a..9c4a680 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
@@ -14,10 +14,10 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.change.TestSubmitRule;
-import com.google.gerrit.server.change.TestSubmitRule.Input;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -29,7 +29,7 @@
private Provider<TestSubmitRule> view;
@Override
- protected RestModifyView<RevisionResource, Input> createView() {
+ protected RestModifyView<RevisionResource, TestSubmitRuleInput> createView() {
return view.get();
}
}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
index 2e7f0df..34d4bdc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
@@ -15,9 +15,9 @@
package com.google.gerrit.sshd.commands;
+import com.google.gerrit.extensions.common.TestSubmitRuleInput;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.change.RevisionResource;
-import com.google.gerrit.server.change.TestSubmitRule.Input;
import com.google.gerrit.server.change.TestSubmitType;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.inject.Inject;
@@ -29,7 +29,7 @@
private Provider<TestSubmitType> view;
@Override
- protected RestModifyView<RevisionResource, Input> createView() {
+ protected RestModifyView<RevisionResource, TestSubmitRuleInput> createView() {
return view.get();
}
}
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index a2947af..48f7767 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -292,7 +292,6 @@
private Injector createSysInjector() {
final List<Module> modules = new ArrayList<>();
modules.add(new DropWizardMetricMaker.RestModule());
- modules.add(new WorkQueue.Module());
modules.add(new ChangeHookRunner.Module());
modules.add(new ReceiveCommitsExecutorModule());
modules.add(new DiffExecutorModule());
@@ -306,13 +305,12 @@
modules.add(new PluginRestApiModule());
modules.add(new RestCacheAdminModule());
modules.add(new GpgModule(config));
- switch (indexType) {
- case LUCENE:
- modules.add(new LuceneIndexModule());
- break;
- default:
- throw new IllegalStateException("unsupported index.type = " + indexType);
- }
+
+ // Index module shutdown must happen before work queue shutdown, otherwise
+ // work queue can get stuck waiting on index futures that will never return.
+ modules.add(createIndexModule());
+
+ modules.add(new WorkQueue.Module());
modules.add(new CanonicalWebUrlModule() {
@Override
protected Class<? extends Provider<String>> provider() {
@@ -332,6 +330,15 @@
return cfgInjector.createChildInjector(modules);
}
+ private Module createIndexModule() {
+ switch (indexType) {
+ case LUCENE:
+ return new LuceneIndexModule();
+ default:
+ throw new IllegalStateException("unsupported index.type = " + indexType);
+ }
+ }
+
private void initIndexType() {
indexType = IndexModule.getIndexType(cfgInjector);
}
diff --git a/lib/BUCK b/lib/BUCK
index 7d19f27..27bc7f7 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -216,8 +216,8 @@
maven_jar(
name = 'truth',
- id = 'com.google.truth:truth:0.27',
- sha1 = 'bd17774d2dc0fffa884d42c07d2537e86c67acd6',
+ id = 'com.google.truth:truth:0.28',
+ sha1 = '0a388c7877c845ff4b8e19689dda5ac9d34622c4',
license = 'DO_NOT_DISTRIBUTE',
exported_deps = [
':guava',
diff --git a/lib/asciidoctor/BUCK b/lib/asciidoctor/BUCK
index ad13313..f9b5b30 100644
--- a/lib/asciidoctor/BUCK
+++ b/lib/asciidoctor/BUCK
@@ -35,8 +35,8 @@
'//gerrit-server:constants',
'//lib:args4j',
'//lib:guava',
- '//lib/lucene:analyzers-common',
- '//lib/lucene:core-and-backward-codecs',
+ '//lib/lucene:lucene-analyzers-common',
+ '//lib/lucene:lucene-core',
],
visibility = ['//tools/eclipse:classpath'],
)
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK
index 7d0a16c..6bc4834 100644
--- a/lib/lucene/BUCK
+++ b/lib/lucene/BUCK
@@ -2,19 +2,8 @@
VERSION = '5.3.1'
-# core and backward-codecs both provide
-# META-INF/services/org.apache.lucene.codecs.Codec, so they must be merged.
-merge_maven_jars(
- name = 'core-and-backward-codecs',
- srcs = [
- ':backward-codecs_jar',
- ':core_jar',
- ],
- visibility = ['PUBLIC'],
-)
-
maven_jar(
- name = 'core_jar',
+ name = 'lucene-core',
id = 'org.apache.lucene:lucene-core:' + VERSION,
sha1 = '36860653d7e09790ada96aeb1970b4ca396ac5d7',
license = 'Apache2.0',
@@ -22,15 +11,14 @@
'META-INF/LICENSE.txt',
'META-INF/NOTICE.txt',
],
- visibility = [],
)
maven_jar(
- name = 'analyzers-common',
+ name = 'lucene-analyzers-common',
id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION,
sha1 = 'bd804dbc1b8f7941018926e940d20d1016b36c4c',
license = 'Apache2.0',
- deps = [':core-and-backward-codecs'],
+ deps = [':lucene-core'],
exclude = [
'META-INF/LICENSE.txt',
'META-INF/NOTICE.txt',
@@ -38,24 +26,11 @@
)
maven_jar(
- name = 'backward-codecs_jar',
- id = 'org.apache.lucene:lucene-backward-codecs:' + VERSION,
- sha1 = '380603f537317a78f9d9b7421bc2ac87586cb9a1',
- license = 'Apache2.0',
- deps = [':core_jar'],
- exclude = [
- 'META-INF/LICENSE.txt',
- 'META-INF/NOTICE.txt',
- ],
- visibility = [],
-)
-
-maven_jar(
- name = 'misc',
+ name = 'lucene-misc',
id = 'org.apache.lucene:lucene-misc:' + VERSION,
sha1 = '7891bbc18b372135c2a52b471075b0bdf5f110ec',
license = 'Apache2.0',
- deps = [':core-and-backward-codecs'],
+ deps = [':lucene-core'],
exclude = [
'META-INF/LICENSE.txt',
'META-INF/NOTICE.txt',
@@ -63,11 +38,11 @@
)
maven_jar(
- name = 'queryparser',
+ name = 'lucene-queryparser',
id = 'org.apache.lucene:lucene-queryparser:' + VERSION,
sha1 = 'bef0e2ac5b196dbab9d0b7c8cc8196b7ef5dd056',
license = 'Apache2.0',
- deps = [':core-and-backward-codecs'],
+ deps = [':lucene-core'],
exclude = [
'META-INF/LICENSE.txt',
'META-INF/NOTICE.txt',
diff --git a/plugins/BUCK b/plugins/BUCK
index a31237d..9948720 100644
--- a/plugins/BUCK
+++ b/plugins/BUCK
@@ -6,6 +6,9 @@
'reviewnotes',
'singleusergroup'
]
+CUSTOM = [
+ # Add custom core plugins here
+]
# buck audit parses and resolves all deps even if not reachable
# from the root(s) passed to audit. Filter dependencies to only
@@ -20,7 +23,7 @@
else:
n.append(p)
return h, n
-HAVE, NEED = core_plugins(CORE)
+HAVE, NEED = core_plugins(CORE + CUSTOM)
genrule(
name = 'core',
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index eea84e7..88b0984 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit eea84e7e07ecf6ebb70ea5a6b0cde67f5a5576af
+Subproject commit 88b0984638857ac7139de83b18dc7cad23670b4d
diff --git a/polygerrit-ui/app/elements/gr-ajax.html b/polygerrit-ui/app/elements/gr-ajax.html
index 9671b37..97f3528 100644
--- a/polygerrit-ui/app/elements/gr-ajax.html
+++ b/polygerrit-ui/app/elements/gr-ajax.html
@@ -79,6 +79,13 @@
},
},
+ ready: function() {
+ // Used for debugging which element a request came from.
+ var headers = this.$.xhr.headers;
+ headers['x-requesting-element-id'] = this.id || 'gr-ajax (no id)';
+ this.$.xhr.headers = headers;
+ },
+
generateRequest: function() {
return this.$.xhr.generateRequest();
},
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index afaf951..afb25e8 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -86,6 +86,7 @@
</style>
<gr-ajax auto url="/accounts/self/detail" last-response="{{account}}"></gr-ajax>
<gr-ajax auto url="/config/server/info" last-response="{{config}}"></gr-ajax>
+ <gr-ajax auto url="/config/server/version" last-response="{{version}}"></gr-ajax>
<header role="banner">
<a href="/" class="bigTitle">PolyGerrit</a>
<div class="headerRightItems">
@@ -110,7 +111,21 @@
<gr-diff-view params="[[params]]"></gr-diff-view>
</template>
</main>
- <footer role="contentinfo">Powered by PolyGerrit</footer>
+ <footer role="contentinfo">
+ Powered by <a href="https://www.gerritcodereview.com/" target="_blank">Gerrit Code Review</a>
+ (<span>[[version]]</span>)
+ <span hidden$="[[!config.gerrit.report_bug_url]]">
+ |
+ <a href$="[[config.gerrit.report_bug_url]]" target="_blank">
+ <span hidden$="[[!config.gerrit.report_bug_text]]">
+ [[config.gerrit.report_bug_text]]
+ </span>
+ <span hidden$="[[config.gerrit.report_bug_text]]">
+ Report Bug
+ </span>
+ </a>
+ </span>
+ </footer>
</template>
<script>
(function() {
@@ -148,6 +163,7 @@
}.bind(this));
},
},
+ version: String,
constrained: {
type: Boolean,
value: false,
@@ -161,7 +177,7 @@
},
get loggedIn() {
- return this.account && Object.keys(this.account).length > 0;
+ return !!(this.account && Object.keys(this.account).length > 0);
},
_accountChanged: function() {
diff --git a/polygerrit-ui/app/elements/gr-change-list-item.html b/polygerrit-ui/app/elements/gr-change-list-item.html
index ef0f864..63f5974 100644
--- a/polygerrit-ui/app/elements/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/gr-change-list-item.html
@@ -71,7 +71,7 @@
<template is="dom-if" if="[[showAvatar]]">
<img class="avatarImage" src$="[[_computeAvatarURL(change.owner)]]">
</template>
- <a href$="[[_computeOwnerLink(change.owner.email)]]"
+ <a href$="[[_computeOwnerLink(change.owner)]]"
title$="[[_computeOwnerTitle(change.owner)]]">[[change.owner.name]]</a>
</span>
<a class="cell project" href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a>
@@ -179,12 +179,13 @@
_computeAvatarURL: function(owner) {
if (!owner) { return ''; }
- return '/accounts/' + owner.email + '/avatar?s=32'
+ return '/accounts/' + owner._account_id + '/avatar?s=32'
},
- _computeOwnerLink: function(email) {
- if (!email) { return ''; }
- return '/q/owner:' + encodeURIComponent(email) + '+status:open';
+ _computeOwnerLink: function(owner) {
+ if (!owner) { return ''; }
+ var ownerID = owner.email || owner._account_id;
+ return '/q/owner:' + encodeURIComponent(ownerID) + '+status:open';
},
_computeOwnerTitle: function(owner) {
diff --git a/polygerrit-ui/app/elements/gr-change-view.html b/polygerrit-ui/app/elements/gr-change-view.html
index b9558f8..1d626e7 100644
--- a/polygerrit-ui/app/elements/gr-change-view.html
+++ b/polygerrit-ui/app/elements/gr-change-view.html
@@ -19,6 +19,7 @@
<link rel="import" href="gr-ajax.html">
<link rel="import" href="gr-date-formatter.html">
<link rel="import" href="gr-file-list.html">
+<link rel="import" href="gr-linked-text.html">
<link rel="import" href="gr-messages-list.html">
<link rel="import" href="gr-reply-dropdown.html">
@@ -38,6 +39,7 @@
height: 4.1em;
}
.header {
+ align-items: center;
background-color: var(--view-background-color);
display: flex;
max-width: var(--max-constrained-width);
@@ -57,6 +59,15 @@
overflow: hidden;
text-overflow: ellipsis;
}
+ .patchSelectLabel {
+ margin-left: var(--default-horizontal-margin);
+ }
+ .header select {
+ margin-left: .5em;
+ }
+ .header gr-reply-dropdown {
+ margin-left: var(--default-horizontal-margin);
+ }
section {
margin: 10px 0;
padding: 10px var(--default-horizontal-margin);
@@ -80,33 +91,50 @@
border-bottom: 1px solid #ddd;
font-family: 'Source Code Pro', Menlo, 'Lucida Console', Monaco, monospace;
overflow-x: auto;
- white-space: pre-wrap;
}
gr-file-list {
padding: 0 var(--default-horizontal-margin) 10px;
}
</style>
<gr-ajax id="detailXHR"
- url="[[_computeDetailPath(changeNum)]]"
+ url="[[_computeDetailPath(_changeNum)]]"
params="[[_computeDetailQueryParams()]]"
- last-response="{{change}}"
+ last-response="{{_change}}"
loading="{{_loading}}"></gr-ajax>
<gr-ajax id="commentsXHR"
- url="[[_computeCommentsPath(changeNum)]]"
- last-response="{{comments}}"></gr-ajax>
+ url="[[_computeCommentsPath(_changeNum)]]"
+ last-response="{{_comments}}"></gr-ajax>
+ <gr-ajax id="commitInfoXHR"
+ url="[[_computeCommitInfoPath(_changeNum, _patchNum)]]"
+ last-response="{{_commitInfo}}"></gr-ajax>
+ <!-- TODO(andybons): Cache the project config. -->
+ <gr-ajax id="configXHR"
+ auto
+ url="[[_computeProjectConfigPath(_change.project)]]"
+ last-response="{{_projectConfig}}"></gr-ajax>
<div class="container loading" hidden$="{{!_loading}}">Loading...</div>
<div class="container" hidden$="{{_loading}}">
<div class="headerContainer">
<div class="header">
<h2>
- <a href$="[[_computeChangePath(change._number)]]">[[change._number]]</a><span>:</span>
- <span>[[change.subject]]</span>
+ <a href$="[[_computeChangePath(_change._number)]]">[[_change._number]]</a><span>:</span>
+ <span>[[_change.subject]]</span>
</h2>
+ <label class="patchSelectLabel" for="patchSetSelect">Patch set</label>
+ <select id="patchSetSelect" on-change="_handlePatchChange">
+ <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber">
+ <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchNum)]]">
+ <span>[[patchNumber]]</span>
+ /
+ <span>[[_computeLatestPatchNum(_change)]]</span>
+ </option>
+ </template>
+ </select>
<gr-reply-dropdown id="replyDropdown"
- change-num="[[changeNum]]"
- patch-num="[[_computePatchNum(change.current_revision)]]"
- labels="[[change.labels]]"
- permitted-labels="[[change.permitted_labels]]"
+ change-num="[[_changeNum]]"
+ patch-num="[[_patchNum]]"
+ labels="[[_change.labels]]"
+ permitted-labels="[[_change.permitted_labels]]"
on-send="_handleReplySent"
hidden$="[[!_loggedIn]]">Reply</gr-reply-dropdown>
</div>
@@ -115,13 +143,13 @@
<table>
<tr>
<td class="changeInfo-label">Owner</td>
- <td>[[change.owner.name]]</td>
+ <td>[[_change.owner.name]]</td>
</tr>
<tr>
<td class="changeInfo-label">Reviewers</td>
<td>
<template is="dom-repeat"
- items="[[_computeReviewers(change.labels, change.owner)]]"
+ items="[[_computeReviewers(_change.labels, _change.owner)]]"
as="reviewer">
<div>[[reviewer.name]]</div>
</template>
@@ -129,15 +157,15 @@
</tr>
<tr>
<td class="changeInfo-label">Project</td>
- <td>[[change.project]]</td>
+ <td>[[_change.project]]</td>
</tr>
<tr>
<td class="changeInfo-label">Branch</td>
- <td>[[change.branch]]</td>
+ <td>[[_change.branch]]</td>
</tr>
<tr>
<td class="changeInfo-label">Topic</td>
- <td>[[change.topic]]</td>
+ <td>[[_change.topic]]</td>
</tr>
<tr>
<td class="changeInfo-label">Strategy</td>
@@ -147,19 +175,24 @@
<td class="changeInfo-label">Updated</td>
<td>
<gr-date-formatter
- date-str="[[change.updated]]"></gr-date-formatter>
+ date-str="[[_change.updated]]"></gr-date-formatter>
</td>
</tr>
</table>
</section>
- <section class="summary">[[_computeCurrentRevisionMessage(change)]]</section>
- <gr-file-list change-num="[[changeNum]]"
- patch-num="[[_computePatchNum(change.current_revision)]]"
- revision="[[change.current_revision]]"
- comments="[[comments]]"></gr-file-list>
- <gr-messages-list change-num="[[changeNum]]"
- messages="[[change.messages]]"
- comments="[[comments]]"></gr-messages-list>
+ <section class="summary">
+ <gr-linked-text pre
+ content="[[_commitInfo.message]]"
+ config="[[_projectConfig.commentlinks]]"></gr-linked-text>
+ </section>
+ <gr-file-list id="fileList"
+ change-num="[[_changeNum]]"
+ patch-num="[[_patchNum]]"
+ comments="[[_comments]]"></gr-file-list>
+ <gr-messages-list
+ change-num="[[_changeNum]]"
+ messages="[[_change.messages]]"
+ comments="[[_comments]]"></gr-messages-list>
</div>
</template>
<script>
@@ -187,8 +220,19 @@
type: Object,
observer: '_paramsChanged',
},
- changeNum: Number,
+ _comments: Object,
+ _change: {
+ type: Object,
+ observer: '_changeChanged',
+ },
+ _commitInfo: Object,
+ _changeNum: String,
+ _patchNum: String,
+ _allPatchSets: {
+ type: Array,
+ computed: '_computeAllPatchSets(_change)',
+ },
_loggedIn: {
type: Boolean,
value: false,
@@ -196,6 +240,8 @@
_loading: Boolean,
_headerContainerEl: Object,
_headerEl: Object,
+ _projectConfig: Object,
+ _scrollHandler: Function,
},
keyBindings: {
@@ -206,11 +252,15 @@
app.accountReady.then(function() {
this._loggedIn = app.loggedIn;
}.bind(this));
+ this._scrollHandler = this._handleBodyScroll.bind(this);
},
attached: function() {
- window.addEventListener('scroll',
- this._handleBodyScroll.bind(this));
+ window.addEventListener('scroll', this._scrollHandler);
+ },
+
+ detached: function() {
+ window.removeEventListener('scroll', this._scrollHandler);
},
_handleBodyScroll: function(e) {
@@ -224,25 +274,44 @@
offsetParent = offsetParent.offsetParent) {
top += offsetParent.offsetTop;
}
+ // The element may not be displayed yet, in which case do nothing.
+ if (top == 0) { return; }
var el = this._headerEl || this.$$('.header');
+ this._headerEl = el;
el.classList.toggle('pinned', window.scrollY >= top);
},
+ _handlePatchChange: function(e) {
+ var patchNum = e.target.value;
+ var currentPatchNum =
+ this._change.revisions[this._change.current_revision]._number
+ if (patchNum == currentPatchNum) {
+ page.show(this._computeChangePath(this._changeNum));
+ return;
+ }
+ page.show(this._computeChangePath(this._changeNum) + '/' + patchNum);
+ },
+
_handleReplySent: function(e) {
this._reload();
},
_paramsChanged: function(value) {
- this.changeNum = value.changeNum;
- if (!this.changeNum) {
- this.change = null;
- this.comments = null;
+ this._changeNum = value.changeNum;
+ this._patchNum = value.patchNum;
+ if (!this._changeNum) {
return;
}
this._reload();
},
+ _changeChanged: function(change) {
+ if (!change) { return; }
+ this._patchNum = this._patchNum ||
+ change.revisions[change.current_revision]._number;
+ },
+
_computeChangePath: function(changeNum) {
return '/c/' + changeNum;
},
@@ -251,32 +320,26 @@
return '/changes/' + changeNum + '/detail';
},
- _computeCommitInfoPath: function(changeNum, commitHash) {
- return '/changes/' + changeNum + '/revisions/' + commitHash + '/commit';
+ _computeCommitInfoPath: function(changeNum, patchNum) {
+ return Changes.baseURL(changeNum, patchNum) + '/commit?links';
},
_computeCommentsPath: function(changeNum) {
return '/changes/' + changeNum + '/comments';
},
- _computePatchNum: function(revision) {
- return this.change && this.change.revisions[revision]._number;
+ _computeProjectConfigPath: function(project) {
+ return '/projects/' + project + '/config';
},
_computeDetailQueryParams: function() {
var options = Changes.listChangesOptionsToHex(
- Changes.ListChangesOption.CURRENT_REVISION,
- Changes.ListChangesOption.CURRENT_COMMIT,
+ Changes.ListChangesOption.ALL_REVISIONS,
Changes.ListChangesOption.CHANGE_ACTIONS
);
return { O: options };
},
- _computeCurrentRevisionMessage: function(change) {
- return change &&
- change.revisions[change.current_revision].commit.message;
- },
-
_computeReviewers: function(labels, owner) {
var reviewers =
(labels['Code-Review'] && labels['Code-Review'].all) || [];
@@ -286,6 +349,22 @@
});
},
+ _computeLatestPatchNum: function(change) {
+ return change.revisions[change.current_revision]._number;
+ },
+
+ _computeAllPatchSets: function(change) {
+ var patchNums = [];
+ for (var rev in change.revisions) {
+ patchNums.push(change.revisions[rev]._number);
+ }
+ return patchNums.sort();
+ },
+
+ _computePatchIndexIsSelected: function(index, patchNum) {
+ return this._allPatchSets[index] == patchNum;
+ },
+
_handleKey: function(e) {
if (util.shouldSupressKeyboardShortcut(e)) { return; }
e.preventDefault();
@@ -300,8 +379,19 @@
},
_reload: function() {
- this.$.detailXHR.generateRequest();
+ var detailCompletes = this.$.detailXHR.generateRequest().completes;
this.$.commentsXHR.generateRequest();
+ var reloadCommitInfoAndFileList = function() {
+ this.$.commitInfoXHR.generateRequest();
+ this.$.fileList.reload();
+ }.bind(this);
+
+ if (this._patchNum) {
+ reloadCommitInfoAndFileList();
+ } else {
+ // The patch number is reliant on the change detail request.
+ detailCompletes.then(reloadCommitInfoAndFileList);
+ }
},
});
diff --git a/polygerrit-ui/app/elements/gr-comment-list.html b/polygerrit-ui/app/elements/gr-comment-list.html
new file mode 100644
index 0000000..3c172e5
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-comment-list.html
@@ -0,0 +1,117 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-comment-list">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ .file {
+ border-top: 1px solid #ddd;
+ font-weight: bold;
+ margin: 10px 0 3px;
+ padding: 10px 0 5px;
+ }
+ .container {
+ display: flex;
+ margin: 5px 0;
+ }
+ .lineNum {
+ margin-right: .35em;
+ min-width: 7em;
+ }
+ .message {
+ flex: 1;
+ white-space: pre-wrap;
+ }
+ </style>
+ <template is="dom-repeat" items="{{_files}}" as="file">
+ <div class="file">
+ <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">[[file]]</a>:
+ </div>
+ <template is="dom-repeat"
+ items="[[_computeCommentsForFile(file)]]" as="comment">
+ <div class="container">
+ <a class="lineNum"
+ href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]">
+ <span hidden$="[[!comment.line]]">
+ <span>[[_computePatchDisplayName(comment)]]</span>
+ Line <span>[[comment.line]]</span>:
+ </span>
+ <span hidden$="[[comment.line]]">
+ File comment:
+ </span>
+ </a>
+ <div class="message">[[comment.message]]</div>
+ </div>
+ </template>
+ </template>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-comment-list',
+
+ properties: {
+ changeNum: Number,
+ comments: {
+ type: Object,
+ observer: '_commentsChanged',
+ },
+ patchNum: Number,
+
+ _files: Array,
+ },
+
+ _commentsChanged: function(value) {
+ this._files = Object.keys(value || {}).sort();
+ },
+
+ _computeFileDiffURL: function(file, changeNum, patchNum) {
+ return '/c/' + changeNum + '/' + patchNum + '/' + file;
+ },
+
+ _computeDiffLineURL: function(file, changeNum, patchNum, comment) {
+ var diffURL = this._computeFileDiffURL(file, changeNum, patchNum);
+ if (comment.line) {
+ // TODO(andybons): This is not correct if the comment is on the base.
+ diffURL += '#' + comment.line;
+ }
+ return diffURL;
+ },
+
+ _computeCommentsForFile: function(file) {
+ return this.comments[file];
+ },
+
+ _computePatchDisplayName: function(comment) {
+ if (comment.side == 'PARENT') {
+ return 'Base, ';
+ }
+ if (comment.patch_set != this.patchNum) {
+ return 'PS' + comment.patch_set + ', ';
+ }
+ return '';
+ }
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-comment.html b/polygerrit-ui/app/elements/gr-diff-comment.html
index 979ba49..d8c2537 100644
--- a/polygerrit-ui/app/elements/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/gr-diff-comment.html
@@ -15,7 +15,6 @@
-->
<link rel="import" href="../bower_components/polymer/polymer.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="gr-date-formatter.html">
<link rel="import" href="gr-request.html">
diff --git a/polygerrit-ui/app/elements/gr-diff-side.html b/polygerrit-ui/app/elements/gr-diff-side.html
new file mode 100644
index 0000000..642a02f
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-diff-side.html
@@ -0,0 +1,413 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-diff-side">
+ <template>
+ <style>
+ :host,
+ .container {
+ display: flex;
+ }
+ .content {
+ width: 80ch;
+ }
+ .lineNum:before,
+ .code:before {
+ /* To ensure the height is non-zero in these elements, a
+ zero-width space is set as its content. The character
+ itself doesn't matter. Just that there is something
+ there. */
+ content: '\200B';
+ }
+ .lineNum {
+ background-color: #eee;
+ color: #666;
+ padding: 0 .75em;
+ text-align: right;
+ }
+ .canComment .lineNum {
+ cursor: pointer;
+ }
+ .canComment .lineNum:hover {
+ background-color: #ccc;
+ }
+ .code {
+ white-space: pre;
+ }
+ .lightHighlight {
+ background-color: var(--light-highlight-color);
+ }
+ hl,
+ .darkHighlight {
+ background-color: var(--dark-highlight-color);
+ }
+ .br:after {
+ /* Line feed */
+ content: '\A';
+ }
+ .tab:before {
+ color: #C62828;
+ /* >> character */
+ content: '\00BB';
+ }
+ .numbers .filler {
+ background-color: #eee;
+ }
+ .contextControl {
+ background-color: #fef;
+ }
+ .contextControl a:link,
+ .contextControl a:visited {
+ display: block;
+ text-decoration: none;
+ }
+ .numbers .contextControl {
+ padding: 0 .75em;
+ text-align: right;
+ }
+ .content .contextControl {
+ text-align: center;
+ }
+ </style>
+ <div class$="[[_computeContainerClass(canComment)]]">
+ <div class="numbers" id="numbers"></div>
+ <div class="content" id="content"></div>
+ </div>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ var CharCode = {
+ LESS_THAN: '<'.charCodeAt(0),
+ GREATER_THAN: '>'.charCodeAt(0),
+ AMPERSAND: '&'.charCodeAt(0),
+ SEMICOLON: ';'.charCodeAt(0),
+ };
+
+ var TAB_REGEX = /\t/g;
+
+ Polymer({
+ is: 'gr-diff-side',
+
+ /**
+ * Fired when an expand context control is clicked.
+ *
+ * @event expand-context
+ */
+
+ properties: {
+ canComment: {
+ type: Boolean,
+ value: false,
+ },
+ content: {
+ type: Array,
+ notify: true,
+ observer: '_contentChanged',
+ },
+ width: {
+ type: Number,
+ observer: '_widthChanged',
+ },
+
+ _lineFeedHTML: {
+ type: String,
+ value: '<span class="style-scope gr-diff-side br"></span>',
+ readOnly: true,
+ },
+ _highlightStartTag: {
+ type: String,
+ value: '<hl class="style-scope gr-diff-side">',
+ readOnly: true,
+ },
+ _highlightEndTag: {
+ type: String,
+ value: '</hl>',
+ readOnly: true,
+ },
+ },
+
+ scrollToLine: function(lineNum) {
+ if (isNaN(lineNum) || lineNum < 1) { return; }
+
+ var el = this.$$('.numbers .lineNum[data-line-num="' + lineNum + '"]');
+ if (!el) { return; }
+
+ // Calculate where the line is relative to the window.
+ var top = el.offsetTop;
+ for (var offsetParent = el.offsetParent;
+ offsetParent;
+ offsetParent = offsetParent.offsetParent) {
+ top += offsetParent.offsetTop;
+ }
+
+ // Scroll the element to the middle of the window. Dividing by a third
+ // instead of half the inner height feels a bit better otherwise the
+ // element appears to be below the center of the window even when it
+ // isn't.
+ window.scrollTo(0, top - (window.innerHeight / 3) - el.offsetHeight);
+ },
+
+ renderLineIndexRange: function(startIndex, endIndex) {
+ this._render(this.content, startIndex, endIndex);
+ },
+
+ hideElementsWithIndex: function(index) {
+ var els = Polymer.dom(this.root).querySelectorAll(
+ '[data-index="' + index + '"]');
+ for (var i = 0; i < els.length; i++) {
+ els[i].setAttribute('hidden', true);
+ }
+ },
+
+ _widthChanged: function(width) {
+ this.$.content.style.width = width + 'ch';
+ },
+
+ _contentChanged: function(diff) {
+ this._clearChildren(this.$.numbers);
+ this._clearChildren(this.$.content);
+ this._render(diff, 0, diff.length - 1);
+ },
+
+ _computeContainerClass: function(canComment) {
+ return 'container' + (canComment ? ' canComment' : '');
+ },
+
+ _clearChildren: function(el) {
+ while (el.firstChild) {
+ el.removeChild(el.firstChild);
+ }
+ },
+
+ _handleContextControlClick: function(context, e) {
+ e.preventDefault();
+ this.fire('expand-context', {bubbles: false, context: context});
+ },
+
+ _render: function(diff, startIndex, endIndex) {
+ var beforeLineEl;
+ var beforeContentEl;
+ if (endIndex != diff.length - 1) {
+ beforeLineEl = this.$$('.numbers [data-index="' + endIndex + '"]');
+ beforeContentEl = this.$$('.content [data-index="' + endIndex + '"]');
+ }
+
+ for (var i = startIndex; i <= endIndex; i++) {
+ if (diff[i].hidden) { continue; }
+
+ switch (diff[i].type) {
+ case 'CODE':
+ this._renderCode(diff[i], i, beforeLineEl, beforeContentEl);
+ break;
+ case 'FILLER':
+ this._renderFiller(diff[i], i, beforeLineEl, beforeContentEl);
+ break;
+ case 'CONTEXT_CONTROL':
+ this._renderContextControl(diff[i], i, beforeLineEl,
+ beforeContentEl);
+ break;
+ }
+ }
+ },
+
+ _renderContextControl: function(control, index, beforeLineEl,
+ beforeContentEl) {
+ var lineEl = this._createElement('div', 'contextControl');
+ lineEl.setAttribute('data-index', index);
+ lineEl.textContent = '@@';
+ var contentEl = this._createElement('div', 'contextControl');
+ contentEl.setAttribute('data-index', index);
+ var a = this._createElement('a');
+ a.href = '#';
+ a.textContent = 'Show ' + control.numLines + ' common ' +
+ (control.numLines == 1 ? 'line' : 'lines') + '...';
+ a.addEventListener('click',
+ this._handleContextControlClick.bind(this, control));
+ contentEl.appendChild(a);
+
+ this.$.numbers.insertBefore(lineEl, beforeLineEl);
+ this.$.content.insertBefore(contentEl, beforeContentEl);
+ },
+
+ _renderFiller: function(filler, index, beforeLineEl, beforeContentEl) {
+ var lineFillerEl = this._createElement('div', 'filler');
+ lineFillerEl.setAttribute('data-index', index);
+ var fillerEl = this._createElement('div', 'filler');
+ fillerEl.setAttribute('data-index', index);
+ var numLines = filler.numLines || 1;
+
+ lineFillerEl.textContent = '\n'.repeat(numLines);
+ for (var i = 0; i < numLines; i++) {
+ var newlineEl = this._createElement('span', 'br');
+ fillerEl.appendChild(newlineEl);
+ }
+
+ this.$.numbers.insertBefore(lineFillerEl, beforeLineEl);
+ this.$.content.insertBefore(fillerEl, beforeContentEl);
+ },
+
+ _renderCode: function(code, index, beforeLineEl,
+ beforeContentEl) {
+ var lineNumEl = this._createElement('div', 'lineNum');
+ lineNumEl.setAttribute('data-line-num', code.lineNum);
+ lineNumEl.setAttribute('data-index', index);
+ var numLines = code.numLines || 1;
+ lineNumEl.textContent = code.lineNum + '\n'.repeat(numLines);
+
+ var contentEl = this._createElement('div', 'code');
+ contentEl.setAttribute('data-line-num', code.lineNum);
+ contentEl.setAttribute('data-index', index);
+
+ if (code.highlight) {
+ contentEl.classList.add(code.intraline.length > 0 ?
+ 'lightHighlight' : 'darkHighlight');
+ }
+
+ var html = util.escapeHTML(code.content);
+ if (code.highlight && code.intraline.length > 0) {
+ html = this._addIntralineHighlights(code.content, html,
+ code.intraline);
+ }
+ if (numLines > 1) {
+ html = this._addNewLines(code.content, html, numLines);
+ }
+ html = this._addTabIndicators(code.content, html);
+
+ // If the html is equivalent to the text then it didn't get highlighted
+ // or escaped. Use textContent which is faster than innerHTML.
+ if (code.content == html) {
+ contentEl.textContent = code.content;
+ } else {
+ contentEl.innerHTML = html;
+ }
+
+ this.$.numbers.insertBefore(lineNumEl, beforeLineEl);
+ this.$.content.insertBefore(contentEl, beforeContentEl);
+ },
+
+ // Advance `index` by the appropriate number of characters that would
+ // represent one source code character and return that index. For
+ // example, for source code '<span>' the escaped html string is
+ // '<span>'. Advancing from index 0 on the prior html string would
+ // return 4, since < maps to one source code character ('<').
+ _advanceChar: function(html, index) {
+ // Any tags don't count as characters
+ while (index < html.length &&
+ html.charCodeAt(index) == CharCode.LESS_THAN) {
+ while (index < html.length &&
+ html.charCodeAt(index) != CharCode.GREATER_THAN) {
+ index++;
+ }
+ index++; // skip the ">" itself
+ }
+ // An HTML entity (e.g., <) counts as one character.
+ if (index < html.length &&
+ html.charCodeAt(index) == CharCode.AMPERSAND) {
+ while (index < html.length &&
+ html.charCodeAt(index) != CharCode.SEMICOLON) {
+ index++;
+ }
+ }
+ return index + 1;
+ },
+
+ _addIntralineHighlights: function(content, html, highlights) {
+ var startTag = this._highlightStartTag;
+ var endTag = this._highlightEndTag;
+
+ for (var i = 0; i < highlights.length; i++) {
+ var hl = highlights[i];
+
+ var htmlStartIndex = 0;
+ for (var j = 0; j < hl.startIndex; j++) {
+ htmlStartIndex = this._advanceChar(html, htmlStartIndex);
+ }
+
+ var htmlEndIndex = 0;
+ if (hl.endIndex != null) {
+ for (var j = 0; j < hl.endIndex; j++) {
+ htmlEndIndex = this._advanceChar(html, htmlEndIndex);
+ }
+ } else {
+ // If endIndex isn't present, continue to the end of the line.
+ htmlEndIndex = html.length;
+ }
+ // The start and end indices could be the same if a highlight is meant
+ // to start at the end of a line and continue onto the next one.
+ // Ignore it.
+ if (htmlStartIndex != htmlEndIndex) {
+ html = html.slice(0, htmlStartIndex) + startTag +
+ html.slice(htmlStartIndex, htmlEndIndex) + endTag +
+ html.slice(htmlEndIndex);
+ }
+ }
+ return html;
+ },
+
+ _addNewLines: function(content, html, numLines) {
+ var htmlIndex = 0;
+ var indices = [];
+ for (var i = 0; i < content.length; i++) {
+ if (i > 0 && i % this.width == 0) {
+ indices.push(htmlIndex);
+ }
+ htmlIndex = this._advanceChar(html, htmlIndex)
+ }
+ var result = html;
+ var linesLeft = numLines;
+ // Since the result string is being altered in place, start from the end
+ // of the string so that the insertion indices are not affected as the
+ // result string changes.
+ for (var i = indices.length - 1; i >= 0; i--) {
+ result = result.slice(0, indices[i]) + this._lineFeedHTML +
+ result.slice(indices[i]);
+ linesLeft--;
+ }
+ // numLines is the total number of lines this code block should take up.
+ // Fill in the remaining ones.
+ for (var i = 0; i < linesLeft; i++) {
+ result += this._lineFeedHTML;
+ }
+ return result;
+ },
+
+ _addTabIndicators: function(content, html) {
+ return html.replace(TAB_REGEX,
+ '<span class="style-scope gr-diff-side tab">\t</span>');
+ },
+
+ _createElement: function(tagName, className) {
+ var el = document.createElement(tagName);
+ // When Shady DOM is being used, these classes are added to account for
+ // Polymer's polyfill behavior. In order to guarantee sufficient
+ // specificity within the CSS rules, these are added to every element.
+ // Since the Polymer DOM utility functions (which would do this
+ // automatically) are not being used for performance reasons, this is
+ // done manually.
+ el.classList.add('style-scope', 'gr-diff-side');
+ if (!!className) {
+ el.classList.add(className);
+ }
+ return el;
+ },
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-diff-view.html b/polygerrit-ui/app/elements/gr-diff-view.html
index 3e616f2..fd8c5e2 100644
--- a/polygerrit-ui/app/elements/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/gr-diff-view.html
@@ -16,8 +16,9 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
+<link rel="import" href="../bower_components/iron-dropdown/iron-dropdown.html">
<link rel="import" href="gr-ajax.html">
-<link rel="import" href="gr-diff-comment-thread.html">
+<link rel="import" href="gr-diff.html">
<dom-module id="gr-diff-view">
<template>
@@ -30,122 +31,100 @@
margin-top: 1em;
padding: .75em var(--default-horizontal-margin);
}
+ .jumpToFileContainer {
+ display: inline-block;
+ }
+ .downArrow {
+ display: inline-block;
+ font-size: .6em;
+ vertical-align: middle;
+ }
+ .dropdown-trigger {
+ color: #00e;
+ cursor: pointer;
+ padding: 0;
+ }
+ .dropdown-content {
+ background-color: #fff;
+ box-shadow: 0 1px 5px rgba(0, 0, 0, .3);
+ }
+ .dropdown-content a {
+ cursor: pointer;
+ display: block;
+ font-weight: normal;
+ padding: .3em .5em;
+ }
+ .dropdown-content a:before {
+ color: #ccc;
+ content: attr(data-key-nav);
+ display: inline-block;
+ margin-right: .5em;
+ width: .3em;
+ }
+ .dropdown-content a:hover {
+ background-color: #00e;
+ color: #fff;
+ }
+ .dropdown-content a[selected] {
+ color: #000;
+ font-weight: bold;
+ pointer-events: none;
+ text-decoration: none;
+ }
+ .dropdown-content a[selected]:hover {
+ background-color: #fff;
+ color: #000;
+ }
+ button {
+ background: none;
+ border: none;
+ font: inherit;
+ padding: .3em 0;
+ }
.mainContainer {
border-bottom: 1px solid #eee;
border-collapse: collapse;
border-top: 1px solid #eee;
width: 100%;
}
- .diffNumbers,
- .diffContent {
- vertical-align: top;
- }
- .diffContainer {
- font-family: 'Source Code Pro', monospace;
- white-space: pre;
- }
- .diffNumbers {
- background-color: #eee;
- color: #666;
- padding: 0 .75em;
- text-align: right;
- }
- .diffContent {
- min-width: 80ch;
- max-width: 120ch;
- overflow: hidden;
- }
- .diffContainer.leftOnly .diffContent,
- .diffContainer.rightOnly .diffContent {
- overflow: visible;
- }
- .diffContainer.leftOnly .right,
- .diffContainer.rightOnly .left {
- display: none;
- }
- .ruler {
- display: block;
- background-color: #ddd;
- height: 1.3em;
- position: absolute;
- top: 0;
- width: 1px;
- }
- .lineNum:before,
- .content:before {
- /* To ensure the height is non-zero in these elements, a
- zero-width space is set as its content. The character
- itself doesn't matter. Just that there is something
- there. */
- content: '\200B';
- }
- .content {
- position: relative;
- }
- .lineNum.blank,
- .threadFiller--redLine {
- border-right: 2px solid #F34D4D;
- margin-right: 3px;
- }
-
- .lineNum:not(.blank) {
- cursor: pointer;
- }
- .lineNum:hover {
- text-decoration: underline;
- }
- .lightRed {
- background-color: #ffecec;
- }
- .darkRed {
- background-color: #faa;
- }
- .lightGreen {
- background-color: #eaffea;
- }
- .darkGreen {
- background-color: #9f9;
- }
</style>
<gr-ajax id="changeDetailXHR"
auto
url="[[_computeChangeDetailPath(_changeNum)]]"
params="[[_computeChangeDetailQueryParams()]]"
last-response="{{_change}}"></gr-ajax>
- <gr-ajax id="diffXHR"
- url="[[_computeDiffPath(_changeNum, _patchNum, _path)]]"
- on-response="_handleDiffResponse"></gr-ajax>
- <gr-ajax id="leftCommentsXHR"
- url="[[_computeCommentsPath(_changeNum, _basePatchNum)]]"
- json-prefix=")]}'"
- on-response="_handleLeftCommentsResponse"></gr-ajax>
- <gr-ajax id="rightCommentsXHR"
- url="[[_computeCommentsPath(_changeNum, _patchNum)]]"
- on-response="_handleRightCommentsResponse"></gr-ajax>
- <!-- TODO(andybons): This is populated in gr-change-view. Use that instead
- of incurring an extra ajax call. -->
<gr-ajax id="filesXHR"
- url="[[_computeFilesPath(_changeNum, _patchNum)]]"
+ auto
+ url="[[_computeFilesPath(_changeNum, _patchRange.patchNum)]]"
on-response="_handleFilesResponse"></gr-ajax>
- <gr-ajax id="leftDraftsXHR"
- url="[[_computeDraftsPath(_changeNum, _basePatchNum)]]"
- on-response="_handleLeftDraftsResponse"></gr-ajax>
- <gr-ajax id="rightDraftsXHR"
- url="[[_computeDraftsPath(_changeNum, _patchNum)]]"
- on-response="_handleRightDraftsResponse"></gr-ajax>
-
<h3>
<a href$="[[_computeChangePath(_changeNum)]]">[[_changeNum]]</a><span>:</span>
- <span>[[_change.subject]]</span> — <span>[[params.path]]</span>
+ <span>[[_change.subject]]</span> —
+ <div class="jumpToFileContainer">
+ <button class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler">
+ <span>[[_path]]</span>
+ <span class="downArrow">▼</span>
+ </button>
+ <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25">
+ <div class="dropdown-content">
+ <template is="dom-repeat" items="[[_fileList]]" as="path">
+ <a href$="[[_computeFileURL(_changeNum, _patchRange.patchNum, path)]]"
+ selected$="[[_computeFileSelected(path, _path)]]"
+ data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]"
+ on-tap="_handleFileTap">[[path]]</a>
+ </template>
+ </div>
+ </iron-dropdown>
+ </div>
</h3>
- <table class="mainContainer">
- <tr class="diffContainer" id="diffContainer">
- <td class="diffNumbers left" id="leftDiffNumbers"></td>
- <td class="diffContent left" id="leftDiffContent"></td>
- <td class="diffNumbers right" id="rightDiffNumbers"></td>
- <td class="diffContent right" id="rightDiffContent"></td>
- </tr>
- </table>
+ <gr-diff id="diff"
+ auto
+ change-num="[[_changeNum]]"
+ patch-range="[[_patchRange]]"
+ path="[[_path]]"
+ available-patches="[[_computeAvailablePatches(_change.revisions)]]"
+ on-render="_handleDiffRender">
+ </gr-diff>
</template>
<script>
(function() {
@@ -172,12 +151,7 @@
type: Object,
observer: '_paramsChanged',
},
- rulerWidth: {
- type: Number,
- value: 80,
- observer: '_rulerWidthChanged',
- },
- _basePatchNum: String,
+ _patchRange: Object,
_change: Object,
_changeNum: String,
_diff: Object,
@@ -185,222 +159,13 @@
type: Array,
value: function() { return []; },
},
- _leftComments: {
- type: Array,
- value: function() { return []; },
- },
- _leftDrafts: {
- type: Array,
- value: function() { return []; },
- },
- _patchNum: String,
_path: String,
- _rendered: Boolean,
- _rightComments: {
- type: Array,
- value: function() { return []; },
- },
- _rightDrafts: {
- type: Array,
- value: function() { return []; },
- },
- },
-
- listeners: {
- 'diffContainer.tap': '_diffContainerTapHandler',
},
keyBindings: {
'[ ] u': '_handleKey',
},
- _paramsChanged: function(value) {
- this._changeNum = value.changeNum;
- this._patchNum = value.patchNum;
- this._basePatchNum = value.basePatchNum;
- this._path = value.path;
- if (!this._patchNum) {
- this._change = null;
- this._basePatchNum = null;
- this._patchNum = null;
- this._diff = null;
- this._path = null;
- this._leftComments = [];
- this._rightComments = [];
- this._leftDrafts = [];
- this._rightDrafts = [];
- this._rendered = false;
- return;
- }
- // Assign the params here since a computed binding relying on
- // `_basePatchNum` won't fire in the case where it's not defined.
- this.$.diffXHR.params = this._diffQueryParams(this._basePatchNum);
-
- var requestPromises = [];
- requestPromises.push(this.$.diffXHR.generateRequest().completes);
-
- if (this._basePatchNum) {
- requestPromises.push(
- this.$.leftCommentsXHR.generateRequest().completes);
- }
-
- requestPromises.push(
- this.$.rightCommentsXHR.generateRequest().completes);
- requestPromises.push(this.$.filesXHR.generateRequest().completes);
-
- app.accountReady.then(function() {
- if (app.loggedIn) {
- if (this._basePatchNum) {
- requestPromises.push(
- this.$.leftDraftsXHR.generateRequest().completes);
- }
- requestPromises.push(
- this.$.rightDraftsXHR.generateRequest().completes);
- }
-
- Promise.all(requestPromises).then(function(requests) {
- this._renderDiff(this._diff, this._leftComments,
- this._rightComments, this._leftDrafts, this._rightDrafts);
- }.bind(this), function(err) {
- alert('Oops. Something went wrong. Check the console and bug the ' +
- 'PolyGerrit team for assistance.');
- throw err;
- });
- }.bind(this));
- },
-
- _rulerWidthChanged: function(newValue, oldValue) {
- if (newValue < 0) {
- throw Error('ruler width must be greater than zero.');
- }
- if (oldValue == 0) {
- this._renderRulerElements();
- }
- var remove = newValue == 0;
- var rulerEls = Polymer.dom(this.root).querySelectorAll('.ruler');
- for (var i = 0; i < rulerEls.length; i++) {
- if (remove) {
- rulerEls[i].parentNode.removeChild(rulerEls[i]);
- } else {
- rulerEls[i].style.left = newValue + 'ch';
- }
- }
- },
-
- _computeChangePath: function(changeNum) {
- return '/c/' + changeNum;
- },
-
- _computeChangeDetailPath: function(changeNum) {
- return '/changes/' + changeNum + '/detail';
- },
-
- _computeChangeDetailQueryParams: function() {
- var options = Changes.listChangesOptionsToHex(
- Changes.ListChangesOption.ALL_REVISIONS
- );
- return { O: options };
- },
-
- _computeDiffPath: function(changeNum, patchNum, path) {
- return '/changes/' + changeNum + '/revisions/' + patchNum + '/files/' +
- encodeURIComponent(path) + '/diff';
- },
-
- _computeCommentsPath: function(changeNum, patchNum) {
- return '/changes/' + changeNum + '/revisions/' + patchNum + '/comments';
- },
-
- _computeFilesPath: function(changeNum, patchNum) {
- return '/changes/' + changeNum + '/revisions/' + patchNum + '/files';
- },
-
- _computeDraftsPath: function(changeNum, patchNum) {
- return '/changes/' + changeNum + '/revisions/' + patchNum + '/drafts';
- },
-
- _diffQueryParams: function(basePatchNum) {
- var params = {
- context: 'ALL',
- intraline: null
- };
- if (!!basePatchNum) {
- params.base = basePatchNum;
- }
- return params;
- },
-
- _diffContainerTapHandler: function(e) {
- var el = e.detail.sourceEvent.target;
- // This tap handler only handles line number taps.
- if (!el.classList.contains('lineNum')) { return; }
-
- var leftSide = el.parentNode == this.$.leftDiffNumbers;
- var rightSide = el.parentNode == this.$.rightDiffNumbers;
- if (leftSide == rightSide) {
- throw Error('Comment tap event cannot originate from both left and ' +
- 'right side');
- }
-
- // If a draft or comment is already present at that line, don’t do
- // anything.
- var lineNum = el.getAttribute('data-line-num');
- var patchNum = el.getAttribute('data-patch-num');
-
- var existingEl = this.$$('gr-diff-comment-thread' +
- '[data-patch-num="' + patchNum + '"]' +
- '[data-line-num="' + lineNum + '"]');
- if (existingEl) {
- // A comment or draft is already present at this line.
- return;
- }
-
- var tempDraftID = Math.floor(Math.random() * Math.pow(10, 10)) + '';
- var drafts = [{
- __draft: true,
- __draftID: tempDraftID,
- path: this._path,
- line: lineNum,
- }];
-
- // If the comment is on the left side of a side-by-side diff with the
- // parent on the left and a patch with patchNum on the right, the patch
- // number passed to the backend is the right side patchNum when mutating
- // a draft. The property `side` is used to determine that it should be
- // on the parent patch, which is inconsistent and why this looks weird.
- var patchNum = this._patchNum;
- if (leftSide && this._basePatchNum == null) {
- drafts[0].side = 'PARENT';
- patchNum = 'PARENT';
- }
-
- this._addThread(drafts, patchNum, lineNum);
- },
-
- _handleLeftCommentsResponse: function(e, req) {
- this._leftComments = e.detail.response[this._path] || [];
- },
-
- _handleRightCommentsResponse: function(e, req) {
- this._rightComments = e.detail.response[this._path] || [];
- },
-
- _handleLeftDraftsResponse: function(e, req) {
- this._leftDrafts = e.detail.response[this._path] || [];
- },
-
- _handleRightDraftsResponse: function(e, req) {
- this._rightDrafts = e.detail.response[this._path] || [];
- },
-
- _handleFilesResponse: function(e, req) {
- this._fileList = Object.keys(e.detail.response).sort();
- },
-
- _handleDiffResponse: function(e, req) {
- this._diff = e.detail.response;
- },
-
_handleKey: function(e) {
if (util.shouldSupressKeyboardShortcut(e)) { return; }
@@ -419,6 +184,13 @@
}
},
+ _handleDiffRender: function() {
+ if (window.location.hash.length > 0) {
+ this.$.diff.scrollToLine(
+ parseInt(window.location.hash.substring(1), 10));
+ }
+ },
+
_navToFile: function(fileList, direction) {
if (fileList.length == 0) { return; }
@@ -427,328 +199,86 @@
page.show(this._computeChangePath(this._changeNum));
return;
}
- page.show(
- this._diffURL(this._changeNum, this._patchNum, fileList[idx]));
+ page.show(this._diffURL(this._changeNum,
+ this._patchRange.patchNum,
+ fileList[idx]));
},
_diffURL: function(changeNum, patchNum, path) {
return '/c/' + changeNum + '/' + patchNum + '/' + path;
},
- _threadID: function(patchNum, lineNum) {
- return 'thread-' + patchNum + '-' + lineNum;
- },
-
- _renderCommentsAndDrafts: function(comments, drafts, patchNum) {
- // Drafts and comments are combined here, with drafts annotated with a
- // property.
- var annotatedDrafts = drafts.map(function(d) {
- d.__draft = true;
- return d;
- });
- comments = comments.concat(annotatedDrafts);
-
- // Group the comments and drafts by line number. Absence of a line
- // number indicates a top-level file comment or draft.
- var threads = {};
-
- for (var i = 0; i < comments.length; i++) {
- var line = comments[i].line || 'FILE';
- if (threads[line] == null) {
- threads[line] = []
- }
- threads[line].push(comments[i]);
- }
- for (var lineNum in threads) {
- this._addThread(threads[lineNum], patchNum, lineNum);
- }
- },
-
- _addThread: function(comments, patchNum, lineNum) {
- var el = document.createElement('gr-diff-comment-thread');
- el.comments = comments;
- el.changeNum = this._changeNum;
- // Assign the element's patchNum to the right side patchNum if the
- // passed patchNum is 'PARENT' due to the odd behavior of the REST API.
- // Don't overwrite patchNum since 'PARENT' is used for other properties.
- el.patchNum = patchNum == 'PARENT' ? this._patchNum : patchNum;
-
- var threadID = this._threadID(patchNum, lineNum);
- el.setAttribute('data-thread-id', threadID);
- el.setAttribute('data-line-num', lineNum);
- el.setAttribute('data-patch-num', patchNum);
-
- // Find the element that the thread should be appended after. In the
- // case of a file comment, it will be appended after the first line.
- // TODO: Show file comment above the file itself.
- var fileComment = lineNum == 'FILE';
- if (fileComment) {
- lineNum = 1;
- }
- var contentEl = this.$$('.content' +
- '[data-patch-num="' + patchNum + '"]' +
- '[data-line-num="' + lineNum + '"]');
- var rowNum = contentEl.getAttribute('data-row-num');
- el.addEventListener('gr-diff-comment-thread-height-changed',
- this._handleCommentThreadHeightChange.bind(this, rowNum, threadID));
- Polymer.dom(contentEl.parentNode).insertBefore(
- el, contentEl.nextSibling);
- },
-
- _handleCommentThreadHeightChange: function(rowNum, threadID, e) {
- // Adjust the filler element heights if they're present in the DOM.
- var els = Polymer.dom(this.root).querySelectorAll(
- '.js-threadFiller[data-thread-id="' + threadID + '"]');
- if (els.length > 0) {
- for (var i = 0; i < els.length; i++) {
- els[i].style.height = e.detail.height + 'px';
- }
- return;
- }
-
- // Create the filler elements if they're not already present.
- var els = Polymer.dom(this.root).querySelectorAll(
- '[data-row-num="' + rowNum + '"]');
- for (var i = 0; i < els.length; i++) {
- // Is this is the column with the comment? Skip if so.
- if (els[i].nextSibling &&
- els[i].nextSibling.tagName == 'GR-DIFF-COMMENT-THREAD') {
- continue;
- }
- var fillerEl = document.createElement('div');
- fillerEl.setAttribute('data-thread-id', threadID);
- fillerEl.classList.add('js-threadFiller');
- if (els[i].classList.contains('lineNum')) {
- fillerEl.classList.add('threadFiller--redLine');
- }
- fillerEl.style.height = e.detail.height + 'px';
- Polymer.dom(els[i].parentNode).insertBefore(
- fillerEl, els[i].nextSibling);
- }
- },
-
- _clearChildren: function(el) {
- while (el.firstChild) {
- el.removeChild(el.firstChild);
- }
- },
-
- _renderDiff: function(
- diff, leftComments, rightComments, leftDrafts, rightDrafts) {
- if (this._rendered) {
- this._clearChildren(this.$.leftDiffNumbers);
- this._clearChildren(this.$.leftDiffContent);
- this._clearChildren(this.$.rightDiffNumbers);
- this._clearChildren(this.$.rightDiffContent);
- }
-
- this.$.diffContainer.classList.toggle('rightOnly',
- diff.change_type == Changes.DiffType.ADDED);
- this.$.diffContainer.classList.toggle('leftOnly',
- diff.change_type == Changes.DiffType.DELETED);
-
- var initialLineNum = 0 + (diff.content.skip || 0);
- var ctx = {
- rowNum: 0,
- left: {
- lineNum: initialLineNum,
- content: '',
- cssClass: '',
- },
- right: {
- lineNum: initialLineNum,
- content: '',
- cssClass: '',
- }
+ _paramsChanged: function(value) {
+ this._changeNum = value.changeNum;
+ this._patchRange = {
+ patchNum: value.patchNum,
+ basePatchNum: value.basePatchNum || 'PARENT',
};
- for (var i = 0; i < diff.content.length; i++) {
- this._addDiffChunk(ctx, diff.content[i]);
- }
+ this._path = value.path;
- if (leftComments) {
- this._renderCommentsAndDrafts(leftComments, leftDrafts,
- this._basePatchNum);
- }
- if (rightComments) {
- this._renderCommentsAndDrafts(rightComments, rightDrafts,
- this._patchNum);
- }
-
- if (this.rulerWidth) {
- this._renderRulerElements();
- }
-
- if (window.location.hash.length > 0) {
- // Allow for the initial rendering to complete before scrolling to the
- // appropriate line.
- this.async(function() {
- this._jumpToLine(parseInt(window.location.hash.substring(1), 10));
- }.bind(this), 1);
- }
-
- this._rendered = true;
- },
-
- _jumpToLine: function(lineNum) {
- if (isNaN(lineNum) || lineNum < 1) { return; }
-
- var el = this.$$(
- '.diffNumbers.right .lineNum[data-line-num="' + lineNum + '"]');
- if (!el) { return; }
-
- // Calculate where the line is relative to the window.
- var top = el.offsetTop;
- for (var offsetParent = el.offsetParent;
- offsetParent;
- offsetParent = offsetParent.offsetParent) {
- top += offsetParent.offsetTop;
- }
-
- // Scroll the element to the middle of the window. Dividing by a third
- // instead of half the inner height feels a bit better otherwise the
- // element appears to be below the center of the window even when it
- // isn't.
- window.scrollTo(0, top - (window.innerHeight / 3) - el.offsetHeight);
- },
-
- _addDiffChunk: function(ctx, diffChunk) {
- // Simplest case where both sides have the same content.
- if (diffChunk.ab) {
- for (var i = 0; i < diffChunk.ab.length; i++) {
- ctx.left.lineNum++;
- ctx.right.lineNum++;
- ctx.left.content = ctx.right.content = diffChunk.ab[i];
- ctx.left.cssClass = ctx.right.cssClass = null;
- this._addRow(ctx);
- }
+ // When navigating away from the page, there is a possibility that the
+ // patch number is no longer a part of the URL (say when navigating to
+ // the top-level change info view) and therefore undefined in `params`.
+ if (!this._patchRange.patchNum) {
return;
}
-
- if (diffChunk.a) {
- ctx.left.cssClass = 'lightRed';
- } else {
- delete(ctx.left.cssClass);
- }
- if (diffChunk.b) {
- ctx.right.cssClass = 'lightGreen';
- } else {
- delete(ctx.right.cssClass);
- }
-
- var aLen = (diffChunk.a && diffChunk.a.length) || 0;
- var bLen = (diffChunk.b && diffChunk.b.length) || 0;
- var maxLen = Math.max(aLen, bLen);
- for (var i = 0; i < maxLen; i++) {
- if (diffChunk.a && i < diffChunk.a.length) {
- ctx.left.lineNum++;
- ctx.left.content = diffChunk.a[i];
- } else {
- delete(ctx.left.content);
- }
- if (diffChunk.b && i < diffChunk.b.length) {
- ctx.right.lineNum++;
- ctx.right.content = diffChunk.b[i];
- } else {
- delete(ctx.right.content);
- }
- this._addRow(ctx);
- }
},
- _addRow: function(ctx) {
- var leftLineNumEl = this._createElement('div', 'lineNum');
- var leftColEl = this._createElement('div', 'content');
- var rightLineNumEl = this._createElement('div', 'lineNum');
- var rightColEl = this._createElement('div', 'content');
-
- [leftColEl,
- rightColEl,
- leftLineNumEl,
- rightLineNumEl].forEach(function(el) {
- el.setAttribute('data-row-num', ctx.rowNum);
- });
-
- [leftLineNumEl, leftColEl].forEach(function(el) {
- el.setAttribute('data-patch-num', this._basePatchNum || 'PARENT');
- }.bind(this));
-
- [rightLineNumEl, rightColEl].forEach(function(el) {
- el.setAttribute('data-patch-num', this._patchNum);
- }.bind(this));
-
- if (ctx.left.content != null) {
- leftLineNumEl.textContent = ctx.left.lineNum;
- [leftLineNumEl, leftColEl].forEach(function(el) {
- el.setAttribute('data-line-num', ctx.left.lineNum);
- });
- } else {
- leftLineNumEl.classList.add('blank');
+ _computeAvailablePatches: function(revisions) {
+ var patchNums = [];
+ for (var rev in revisions) {
+ patchNums.push(revisions[rev]._number);
}
- if (ctx.right.content != null) {
- rightLineNumEl.textContent = ctx.right.lineNum;
- [rightLineNumEl, rightColEl].forEach(function(el) {
- el.setAttribute('data-line-num', ctx.right.lineNum);
- });
- } else {
- rightLineNumEl.classList.add('blank');
- }
-
- // Content must be defined to prevent the HTML from showing 'undefined'.
- // Additionally, a newline is used place of an empty string to ensure
- // copy/paste works correctly.
- ctx.left.content = ctx.left.content || '\n';
- ctx.right.content = ctx.right.content || '\n';
-
- if (!!ctx.left.cssClass) {
- leftColEl.classList.add(ctx.left.cssClass);
- }
- if (!!ctx.right.cssClass) {
- rightColEl.classList.add(ctx.right.cssClass);
- }
-
- var leftHTML = util.escapeHTML(ctx.left.content);
- var rightHTML = util.escapeHTML(ctx.right.content);
-
- // If the html is equivalent to the text then it didn't get highlighted
- // or escaped. Use textContent which is faster than innerHTML.
- if (ctx.left.content == leftHTML) {
- leftColEl.textContent = ctx.left.content;
- } else {
- leftColEl.innerHTML = leftHTML;
- }
- if (ctx.right.content == rightHTML) {
- rightColEl.textContent = ctx.right.content;
- } else {
- rightColEl.innerHTML = rightHTML;
- }
-
- this.$.leftDiffNumbers.appendChild(leftLineNumEl);
- this.$.leftDiffContent.appendChild(leftColEl);
- this.$.rightDiffNumbers.appendChild(rightLineNumEl);
- this.$.rightDiffContent.appendChild(rightColEl);
-
- ctx.rowNum++;
+ return patchNums.sort(function(a, b) { return a - b; });
},
- _renderRulerElements: function() {
- var contentEls = Polymer.dom(this.root).querySelectorAll('.content');
- for (var i = 0; i < contentEls.length; i++) {
- var rulerEl = this._createElement('i', 'ruler');
- rulerEl.style.left = this.rulerWidth + 'ch';
- contentEls[i].appendChild(rulerEl);
- }
+ _computeChangePath: function(changeNum) {
+ return '/c/' + changeNum;
},
- _createElement: function(tagName, className) {
- var el = document.createElement(tagName);
- // When Shady DOM is being used, these classes are added to account for
- // Polymer's polyfill behavior. In order to guarantee sufficient
- // specificity within the CSS rules, these are added to every element.
- // Since the Polymer DOM utility functions (which would do this
- // automatically) are not being used for performance reasons, this is
- // done manually.
- el.className = 'style-scope gr-diff-view ' + className;
- return el;
+ _computeChangeDetailPath: function(changeNum) {
+ return '/changes/' + changeNum + '/detail';
+ },
+
+ _computeChangeDetailQueryParams: function() {
+ return { O: Changes.listChangesOptionsToHex(
+ Changes.ListChangesOption.ALL_REVISIONS
+ )};
+ },
+
+ _computeFilesPath: function(changeNum, patchNum) {
+ return Changes.baseURL(changeNum, patchNum) + '/files';
+ },
+
+ _computeFileURL: function(changeNum, patchNum, path) {
+ return '/c/' + changeNum + '/' + patchNum + '/' + path;
+ },
+
+ _computeFileSelected: function(path, currentPath) {
+ return path == currentPath;
+ },
+
+ _computeKeyNav: function(path, selectedPath, fileList) {
+ var selectedIndex = fileList.indexOf(selectedPath);
+ if (fileList.indexOf(path) == selectedIndex - 1) {
+ return '[';
+ }
+ if (fileList.indexOf(path) == selectedIndex + 1) {
+ return ']';
+ }
+ return '';
+ },
+
+ _handleFileTap: function(e) {
+ this.$.dropdown.close();
+ },
+
+ _handleFilesResponse: function(e, req) {
+ this._fileList = Object.keys(e.detail.response).sort();
+ },
+
+ _showDropdownTapHandler: function(e) {
+ this.$.dropdown.open();
},
});
})();
diff --git a/polygerrit-ui/app/elements/gr-diff.html b/polygerrit-ui/app/elements/gr-diff.html
new file mode 100644
index 0000000..a4e9038
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-diff.html
@@ -0,0 +1,530 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="gr-ajax.html">
+<link rel="import" href="gr-diff-side.html">
+<link rel="import" href="gr-patch-range-select.html">
+<link rel="import" href="gr-request.html">
+
+<dom-module id="gr-diff">
+ <template>
+ <style>
+ .header {
+ display: flex;
+ margin: 0 var(--default-horizontal-margin) .75em;
+ }
+ .contextControl {
+ flex: 1;
+ text-align: right;
+ }
+ .diffContainer {
+ border-bottom: 1px solid #eee;
+ border-top: 1px solid #eee;
+ display: flex;
+ font-family: 'Source Code Pro', monospace;
+ overflow-x: auto;
+ white-space: pre;
+ }
+ gr-diff-side:first-of-type {
+ --light-highlight-color: #fee;
+ --dark-highlight-color: #ffd4d4;
+ }
+ gr-diff-side:last-of-type {
+ --light-highlight-color: #efe;
+ --dark-highlight-color: #d4ffd4;
+ border-right: 1px solid #ddd;
+ }
+ </style>
+ <gr-ajax id="diffXHR"
+ url="[[_computeDiffPath(changeNum, patchRange.patchNum, path)]]"
+ params="[[_computeDiffQueryParams(patchRange.basePatchNum)]]"
+ last-response="{{_diffResponse}}"></gr-ajax>
+ <gr-ajax id="baseCommentsXHR"
+ url="[[_computeCommentsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
+ <gr-ajax id="commentsXHR"
+ url="[[_computeCommentsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
+ <gr-ajax id="baseDraftsXHR"
+ url="[[_computeDraftsPath(changeNum, patchRange.basePatchNum)]]"></gr-ajax>
+ <gr-ajax id="draftsXHR"
+ url="[[_computeDraftsPath(changeNum, patchRange.patchNum)]]"></gr-ajax>
+
+ <div class="header">
+ <gr-patch-range-select
+ path="[[path]]"
+ change-num="[[changeNum]]"
+ patch-range="[[patchRange]]"
+ available-patches="[[availablePatches]]"></gr-patch-range-select>
+ <div class="contextControl">
+ Context:
+ <select id="contextSelect" on-change="_handleContextSelectChange">
+ <option value="3">3 lines</option>
+ <option value="10">10 lines</option>
+ <option value="25">25 lines</option>
+ <option value="50">50 lines</option>
+ <option value="75">75 lines</option>
+ <option value="100">100 lines</option>
+ <option value="ALL">Whole file</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="diffContainer">
+ <gr-diff-side id="leftDiff"
+ content="{{_diff.leftSide}}"
+ width="[[sideWidth]]"
+ can-comment="[[_loggedIn]]"
+ on-expand-context="_handleExpandContext"></gr-diff-side>
+ <gr-diff-side id="rightDiff"
+ content="{{_diff.rightSide}}"
+ width="[[sideWidth]]"
+ can-comment="[[_loggedIn]]"
+ on-expand-context="_handleExpandContext"></gr-diff-side>
+ </div>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-diff',
+
+ /**
+ * Fired when the diff is rendered.
+ *
+ * @event render
+ */
+
+ properties: {
+ auto: {
+ type: Boolean,
+ value: false,
+ },
+ availablePatches: Array,
+ changeNum: String,
+ /*
+ * A single object to encompass basePatchNum and patchNum is used
+ * so that both can be set at once without incremental observers
+ * firing after each property changes.
+ */
+ patchRange: Object,
+ path: String,
+ sideWidth: {
+ type: Number,
+ value: 80,
+ },
+ _comments: Array,
+ _context: {
+ type: Number,
+ value: 10,
+ observer: '_contextChanged',
+ },
+ _baseComments: Array,
+ _drafts: Array,
+ _baseDrafts: Array,
+ _diffResponse: Object,
+ _diff: {
+ type: Object,
+ value: function() { return {}; },
+ },
+ _loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+ _diffRequestsPromise: Object, // Used for testing.
+ },
+
+ observers: [
+ '_diffOptionsChanged(changeNum, patchRange, path)'
+ ],
+
+ ready: function() {
+ app.accountReady.then(function() {
+ this._loggedIn = app.loggedIn;
+ }.bind(this));
+ },
+
+ scrollToLine: function(lineNum) {
+ // TODO(andybons): Should this always be the right side?
+ this.$.rightDiff.scrollToLine(lineNum);
+ },
+
+ _contextChanged: function(context) {
+ if (context == Infinity) {
+ this.$.contextSelect.value = 'ALL';
+ } else {
+ this.$.contextSelect.value = context;
+ }
+ },
+
+ _diffOptionsChanged: function(changeNum, patchRange, path) {
+ if (!this.auto) { return; }
+
+ var promises = [this.$.diffXHR.generateRequest().completes];
+
+ var basePatchNum = patchRange.basePatchNum;
+ var patchNum = patchRange.patchNum;
+
+ app.accountReady.then(function() {
+ promises.push(this._getCommentsAndDrafts(basePatchNum, app.loggedIn));
+ this._diffRequestsPromise = Promise.all(promises).then(function() {
+ this._render();
+ }.bind(this)).catch(function(err) {
+ alert('Oops. Something went wrong. Check the console and bug the ' +
+ 'PolyGerrit team for assistance.');
+ throw err;
+ });
+ }.bind(this));
+ },
+
+ _render: function() {
+ this._processContent();
+
+ // Allow for the initial rendering to complete before firing the event.
+ this.async(function() {
+ this.fire('render', {bubbles: false});
+ }.bind(this), 1);
+ },
+
+ _getCommentsAndDrafts: function(basePatchNum, loggedIn) {
+ var promises = [];
+
+ function onlyParent(c) { return c.side == 'PARENT'; }
+ function withoutParent(c) { return c.side != 'PARENT'; }
+
+ var promises = [];
+ var commentsPromise = this.$.commentsXHR.generateRequest().completes;
+ promises.push(commentsPromise.then(function(req) {
+ var comments = req.response[this.path] || [];
+ if (basePatchNum == 'PARENT') {
+ this._baseComments = comments.filter(onlyParent);
+ }
+ this._comments = comments.filter(withoutParent);
+ }.bind(this)));
+
+ if (basePatchNum != 'PARENT') {
+ commentsPromise = this.$.baseCommentsXHR.generateRequest().completes;
+ promises.push(commentsPromise.then(function(req) {
+ this._baseComments =
+ (req.response[this.path] || []).filter(withoutParent);
+ }.bind(this)));
+ }
+
+ if (!loggedIn) {
+ this._baseDrafts = [];
+ this._drafts = [];
+ return Promise.all(promises);
+ }
+
+ var draftsPromise = this.$.draftsXHR.generateRequest().completes;
+ promises.push(draftsPromise.then(function(req) {
+ var drafts = req.response[this.path] || [];
+ if (basePatchNum == 'PARENT') {
+ this._baseDrafts = drafts.filter(onlyParent);
+ }
+ this._drafts = drafts.filter(withoutParent);
+ }.bind(this)));
+
+ if (basePatchNum != 'PARENT') {
+ draftsPromise = this.$.baseDraftsXHR.generateRequest().completes;
+ promises.push(draftsPromise.then(function(req) {
+ this._baseDrafts =
+ (req.response[this.path] || []).filter(withoutParent);
+ }.bind(this)));
+ }
+
+ return Promise.all(promises);
+ },
+
+ _computeDiffPath: function(changeNum, patchNum, path) {
+ return Changes.baseURL(changeNum, patchNum) + '/files/' +
+ encodeURIComponent(path) + '/diff';
+ },
+
+ _computeCommentsPath: function(changeNum, patchNum) {
+ return Changes.baseURL(changeNum, patchNum) + '/comments';
+ },
+
+ _computeDraftsPath: function(changeNum, patchNum) {
+ return Changes.baseURL(changeNum, patchNum) + '/drafts';
+ },
+
+ _computeDiffQueryParams: function(basePatchNum) {
+ var params = {
+ context: 'ALL',
+ intraline: null
+ };
+ if (basePatchNum != 'PARENT') {
+ params.base = basePatchNum;
+ }
+ return params;
+ },
+
+ _handleContextSelectChange: function(e) {
+ if (e.target.value == 'ALL') {
+ this._context = Infinity;
+ } else {
+ this._context = parseInt(e.target.value, 10);
+ }
+ this._render();
+ },
+
+ _handleExpandContext: function(e) {
+ var ctx = e.detail.context;
+ var contextControlIndex = -1;
+ for (var i = ctx.start; i <= ctx.end; i++) {
+ this._diff.leftSide[i].hidden = false;
+ this._diff.rightSide[i].hidden = false;
+ if (this._diff.leftSide[i].type == 'CONTEXT_CONTROL' &&
+ this._diff.rightSide[i].type == 'CONTEXT_CONTROL') {
+ contextControlIndex = i;
+ }
+ }
+ this._diff.leftSide[contextControlIndex].hidden = true;
+ this._diff.rightSide[contextControlIndex].hidden = true;
+
+ this.$.leftDiff.hideElementsWithIndex(contextControlIndex);
+ this.$.rightDiff.hideElementsWithIndex(contextControlIndex);
+
+ this.$.leftDiff.renderLineIndexRange(ctx.start, ctx.end);
+ this.$.rightDiff.renderLineIndexRange(ctx.start, ctx.end);
+ },
+
+ _processContent: function() {
+ var leftSide = [];
+ var rightSide = [];
+ var initialLineNum = 0 + (this._diffResponse.content.skip || 0);
+ var ctx = {
+ hidingLines: false,
+ lastNumLinesHidden: 0,
+ left: {
+ lineNum: initialLineNum,
+ },
+ right: {
+ lineNum: initialLineNum,
+ }
+ };
+ var content = this._diffResponse.content;
+ for (var i = 0; i < content.length; i++) {
+ if (i == 0) {
+ ctx.skipRange = [0, this._context];
+ } else if (i == content.length - 1) {
+ ctx.skipRange = [this._context, 0];
+ } else {
+ ctx.skipRange = [this._context, this._context];
+ }
+ ctx.diffChunkIndex = i;
+ this._addDiffChunk(ctx, content[i], leftSide, rightSide);
+ }
+ this._diff = {
+ leftSide: leftSide,
+ rightSide: rightSide,
+ };
+ },
+
+ _addContextControl: function(ctx, leftSide, rightSide) {
+ var numLinesHidden = ctx.lastNumLinesHidden;
+ var leftStart = leftSide.length - numLinesHidden;
+ var leftEnd = leftSide.length;
+ var rightStart = rightSide.length - numLinesHidden;
+ var rightEnd = rightSide.length;
+ if (leftStart != rightStart || leftEnd != rightEnd) {
+ throw Error(
+ 'Left and right ranges for context control should be equal:' +
+ 'Left: [' + leftStart + ', ' + leftEnd + '] ' +
+ 'Right: ['+ rightStart + ', ' + rightEnd + ']');
+ }
+ var obj = {
+ type: 'CONTEXT_CONTROL',
+ numLines: numLinesHidden,
+ start: leftStart,
+ end: leftEnd,
+ };
+ // NOTE: Be careful, here. This object is meant to be immutable. If the
+ // object is altered within one side's array it will reflect the
+ // alterations in another.
+ leftSide.push(obj);
+ rightSide.push(obj);
+ },
+
+ _addCommonDiffChunk: function(ctx, chunk, leftSide, rightSide) {
+ for (var i = 0; i < chunk.ab.length; i++) {
+ var numLines = Math.ceil(chunk.ab[i].length / this.sideWidth);
+ var hidden = i >= ctx.skipRange[0] &&
+ i < chunk.ab.length - ctx.skipRange[1];
+ if (ctx.hidingLines && hidden == false) {
+ // No longer hiding lines. Add a context control.
+ this._addContextControl(ctx, leftSide, rightSide);
+ ctx.lastNumLinesHidden = 0;
+ }
+ ctx.hidingLines = hidden;
+ if (hidden) {
+ ctx.lastNumLinesHidden++;
+ }
+
+ // Blank lines within a diff content array indicate a newline.
+ leftSide.push({
+ type: 'CODE',
+ hidden: hidden,
+ content: chunk.ab[i] || '\n',
+ numLines: numLines,
+ lineNum: ++ctx.left.lineNum,
+ });
+ rightSide.push({
+ type: 'CODE',
+ hidden: hidden,
+ content: chunk.ab[i] || '\n',
+ numLines: numLines,
+ lineNum: ++ctx.right.lineNum,
+ });
+ }
+ if (ctx.lastNumLinesHidden > 0) {
+ this._addContextControl(ctx, leftSide, rightSide);
+ }
+ },
+
+ _addDiffChunk: function(ctx, chunk, leftSide, rightSide) {
+ if (chunk.ab) {
+ this._addCommonDiffChunk(ctx, chunk, leftSide, rightSide);
+ return;
+ }
+
+ var leftHighlights = [];
+ if (chunk.edit_a) {
+ leftHighlights =
+ this._normalizeIntralineHighlights(chunk.a, chunk.edit_a);
+ }
+ var rightHighlights = [];
+ if (chunk.edit_b) {
+ rightHighlights =
+ this._normalizeIntralineHighlights(chunk.b, chunk.edit_b);
+ }
+
+ var aLen = (chunk.a && chunk.a.length) || 0;
+ var bLen = (chunk.b && chunk.b.length) || 0;
+ var maxLen = Math.max(aLen, bLen);
+ for (var i = 0; i < maxLen; i++) {
+ var hasLeftContent = chunk.a && i < chunk.a.length;
+ var hasRightContent = chunk.b && i < chunk.b.length;
+ var leftContent = hasLeftContent ? chunk.a[i] : '';
+ var rightContent = hasRightContent ? chunk.b[i] : '';
+ var maxNumLines = this._maxLinesSpanned(leftContent, rightContent);
+ if (hasLeftContent) {
+ leftSide.push({
+ type: 'CODE',
+ content: leftContent || '\n',
+ numLines: maxNumLines,
+ lineNum: ++ctx.left.lineNum,
+ highlight: true,
+ intraline: leftHighlights.filter(function(hl) {
+ return hl.contentIndex == i;
+ }),
+ });
+ } else {
+ leftSide.push({
+ type: 'FILLER',
+ numLines: maxNumLines,
+ });
+ }
+ if (hasRightContent) {
+ rightSide.push({
+ type: 'CODE',
+ content: rightContent || '\n',
+ numLines: maxNumLines,
+ lineNum: ++ctx.right.lineNum,
+ highlight: true,
+ intraline: rightHighlights.filter(function(hl) {
+ return hl.contentIndex == i;
+ }),
+ });
+ } else {
+ rightSide.push({
+ type: 'FILLER',
+ numLines: maxNumLines,
+ });
+ }
+ }
+ },
+
+
+ // The `highlights` array consists of a list of <skip length, mark length>
+ // pairs, where the skip length is the number of characters between the
+ // end of the previous edit and the start of this edit, and the mark
+ // length is the number of edited characters following the skip. The start
+ // of the edits is from the beginning of the related diff content lines.
+ //
+ // Note that the implied newline character at the end of each line is
+ // included in the length calculation, and thus it is possible for the
+ // edits to span newlines.
+ //
+ // A line highlight object consists of three fields:
+ // - contentIndex: The index of the diffChunk `content` field (the line
+ // being referred to).
+ // - startIndex: Where the highlight should begin.
+ // - endIndex: (optional) Where the highlight should end. If omitted, the
+ // highlight is meant to be a continuation onto the next line.
+ _normalizeIntralineHighlights: function(content, highlights) {
+ var contentIndex = 0;
+ var idx = 0;
+ var normalized = [];
+ for (var i = 0; i < highlights.length; i++) {
+ var line = content[contentIndex] + '\n';
+ var hl = highlights[i];
+ var j = 0;
+ while (j < hl[0]) {
+ if (idx == line.length) {
+ idx = 0;
+ line = content[++contentIndex] + '\n';
+ continue;
+ }
+ idx++;
+ j++;
+ }
+ var lineHighlight = {
+ contentIndex: contentIndex,
+ startIndex: idx,
+ };
+
+ j = 0;
+ while (line && j < hl[1]) {
+ if (idx == line.length) {
+ idx = 0;
+ line = content[++contentIndex] + '\n';
+ normalized.push(lineHighlight);
+ lineHighlight = {
+ contentIndex: contentIndex,
+ startIndex: idx,
+ };
+ continue;
+ }
+ idx++;
+ j++;
+ }
+ lineHighlight.endIndex = idx;
+ normalized.push(lineHighlight);
+ }
+ return normalized;
+ },
+
+ _maxLinesSpanned: function(left, right) {
+ return Math.max(Math.ceil(left.length / this.sideWidth),
+ Math.ceil(right.length / this.sideWidth));
+ },
+
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-file-list.html b/polygerrit-ui/app/elements/gr-file-list.html
index 9a504b6..d685b4f 100644
--- a/polygerrit-ui/app/elements/gr-file-list.html
+++ b/polygerrit-ui/app/elements/gr-file-list.html
@@ -43,16 +43,25 @@
.status {
width: 20px;
}
+ .drafts {
+ color: #C62828;
+ font-weight: bold;
+ }
</style>
- <gr-ajax id="xhr"
- url="[[_computeFilesURL(changeNum, revision)]]"
+ <gr-ajax id="filesXHR"
+ url="[[_computeFilesURL(changeNum, patchNum)]]"
on-response="_handleResponse"></gr-ajax>
+ <gr-ajax id="draftsXHR"
+ url="[[_computeDraftsURL(changeNum, patchNum)]]"
+ last-response="{{_drafts}}"></gr-ajax>
+ </gr-ajax>
<div class="tableContainer">
<table>
<tr>
<th></th>
<th></th>
<th>Path</th>
+ <th>Comments</th>
<th>Stats</th>
</tr>
<template is="dom-repeat" items="{{files}}" as="file">
@@ -64,6 +73,10 @@
href$="[[_computeDiffURL(changeNum, patchNum, file.__path)]]">[[file.__path]]</a>
</td>
<td>
+ <span class="drafts">[[_computeDraftsString(_drafts, file.__path)]]</span>
+ <span class="comments">[[_computeCommentsString(comments, patchNum, file.__path)]]</span>
+ </td>
+ <td>
+<span>[[file.lines_inserted]]</span> lines,
-<span>[[file.lines_deleted]]</span> lines
</td>
@@ -80,29 +93,46 @@
is: 'gr-file-list',
properties: {
- patchNum: Number,
- changeNum: {
- type: Number,
- observer: '_changeNumOrRevisionChanged',
- },
- revision: {
- type: String,
- observer: '_changeNumOrRevisionChanged',
- },
- comments: {
- type: Array,
- value: function() { return []; },
- },
+ patchNum: String,
+ changeNum: String,
+ comments: Object,
+
+ _drafts: Object,
},
- _changeNumOrRevisionChanged: function() {
- if (!!this.changeNum && !!this.revision) {
- this.$.xhr.generateRequest();
+ reload: function() {
+ if (!!this.changeNum && !!this.patchNum) {
+ this.$.filesXHR.generateRequest();
+ app.accountReady.then(function() {
+ if (!app.loggedIn) { return; }
+ this.$.draftsXHR.generateRequest();
+ }.bind(this));
}
},
- _computeFilesURL: function(changeNum, revision) {
- return '/changes/' + changeNum + '/revisions/' + revision + '/files';
+ _computeFilesURL: function(changeNum, patchNum) {
+ return Changes.baseURL(changeNum, patchNum) + '/files';
+ },
+
+ _computeCommentsString: function(comments, patchNum, path) {
+ var patchComments = (comments[path] || []).filter(function(c) {
+ return c.patch_set == patchNum;
+ });
+ var num = patchComments.length;
+ if (num == 0) { return ''; }
+ if (num == 1) { return '1 comment'; }
+ if (num > 1) { return num + ' comments'; };
+ },
+
+ _computeDraftsURL: function(changeNum, patchNum) {
+ return Changes.baseURL(changeNum, patchNum) + '/drafts';
+ },
+
+ _computeDraftsString: function(drafts, path) {
+ var num = (drafts[path] || []).length;
+ if (num == 0) { return ''; }
+ if (num == 1) { return '1 draft'; }
+ if (num > 1) { return num + ' drafts'; };
},
_handleResponse: function(e, req) {
diff --git a/polygerrit-ui/app/elements/gr-linked-text.html b/polygerrit-ui/app/elements/gr-linked-text.html
new file mode 100644
index 0000000..81db145
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-linked-text.html
@@ -0,0 +1,63 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+<script src="../scripts/ba-linkify.js"></script>
+<script src="../scripts/link-text-parser.js"></script>
+<dom-module id="gr-linked-text">
+ <template>
+ <style>
+ :host([pre]) div {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ }
+ </style>
+ <div id="output"></div>
+ </template>
+ <script>
+ 'use strict';
+
+ Polymer({
+ is: 'gr-linked-text',
+
+ properties: {
+ content: String,
+ config: Object,
+ },
+
+ observers: [
+ '_contentChanged(content, config)',
+ ],
+
+ _contentChanged: function(content, config) {
+ var output = this.$.output;
+ output.textContent = '';
+ var parser = new GrLinkTextParser(config, function(text, href) {
+ if (href) {
+ var a = document.createElement('a');
+ a.href = href;
+ a.textContent = text;
+ a.target = '_blank';
+ output.appendChild(a);
+ } else {
+ output.appendChild(document.createTextNode(text));
+ }
+ });
+ parser.parse(content);
+ }
+ });
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-message.html b/polygerrit-ui/app/elements/gr-message.html
index f07542c..a762673 100644
--- a/polygerrit-ui/app/elements/gr-message.html
+++ b/polygerrit-ui/app/elements/gr-message.html
@@ -15,6 +15,7 @@
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="gr-comment-list.html">
<link rel="import" href="gr-date-formatter.html">
<dom-module id="gr-message">
@@ -78,7 +79,7 @@
.collapsed .message {
display: inline;
}
- .collapsed .comments {
+ .collapsed gr-comment-list {
display: none;
}
.collapsed .name,
@@ -88,8 +89,7 @@
.expanded .name {
cursor: pointer;
}
- .expanded .message,
- .expanded .commentMessage {
+ .expanded .message {
white-space: pre-wrap;
}
gr-date-formatter {
@@ -97,23 +97,6 @@
right: var(--default-horizontal-margin);
top: 10px;
}
- .file {
- border-top: 1px solid #ddd;
- font-weight: bold;
- margin: 10px 0 3px;
- padding: 10px 0 5px;
- }
- .commentContainer {
- display: flex;
- margin: 5px 0;
- }
- .lineNum {
- margin-right: 10px;
- min-width: 75px;
- }
- .commentMessage {
- flex: 1;
- }
</style>
<div class$="[[_computeClass(expanded, showAvatar)]]">
<img class="avatar" src$="[[_computeAvatarURL(message.author, showAvatar)]]">
@@ -121,28 +104,10 @@
<div class="name" id="name">[[message.author.name]]</div>
<div class="content">
<div class="message">[[message.message]]</div>
- <div class="comments">
- <template is="dom-repeat" items="{{files}}" as="file">
- <div class="file">
- <a href$="[[_computeFileDiffURL(file, changeNum, message._revision_number)]]">[[file]]</a>:
- </div>
- <template is="dom-repeat"
- items="[[_computeCommentsForFile(file)]]" as="comment">
- <div class="commentContainer">
- <a class="lineNum"
- href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]">
- <template is="dom-if" if="[[comment.line]]">
- Line <span>[[comment.line]]</span>:
- </template>
- <template is="dom-if" if="[[!comment.line]]">
- File comment:
- </template>
- </a>
- <div class="commentMessage">[[comment.message]]</div>
- </div>
- </template>
- </template>
- </div>
+ <gr-comment-list
+ comments="[[comments]]"
+ change-num="[[changeNum]]"
+ patch-num="[[message._revision_number]]"></gr-comment-list>
</div>
<gr-date-formatter date-str="[[message.date]]"></gr-date-formatter>
</div>
@@ -185,24 +150,7 @@
},
_commentsChanged: function(value) {
- this.files = Object.keys(value || {}).sort();
- this.expanded = this.files.length > 0;
- },
-
- _computeFileDiffURL: function(file, changeNum, patchNum) {
- return '/c/' + changeNum + '/' + patchNum + '/' + file;
- },
-
- _computeDiffLineURL: function(file, changeNum, patchNum, comment) {
- var diffURL = this._computeFileDiffURL(file, changeNum, patchNum);
- if (comment.line) {
- diffURL += '#' + comment.line;
- }
- return diffURL;
- },
-
- _computeCommentsForFile: function(file) {
- return this.comments[file];
+ this.expanded = Object.keys(value || {}).length > 0;
},
_tapHandler: function(e) {
@@ -225,7 +173,7 @@
_computeAvatarURL: function(author, showAvatar) {
if (!showAvatar || !author) { return '' }
- return '/accounts/' + author.email + '/avatar?s=100';
+ return '/accounts/' + author._account_id + '/avatar?s=100';
},
});
diff --git a/polygerrit-ui/app/elements/gr-patch-range-select.html b/polygerrit-ui/app/elements/gr-patch-range-select.html
new file mode 100644
index 0000000..a2a5dc4
--- /dev/null
+++ b/polygerrit-ui/app/elements/gr-patch-range-select.html
@@ -0,0 +1,95 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<link rel="import" href="../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-patch-range-select">
+ <template>
+ <style>
+ :host {
+ display: block;
+ }
+ .patchRange {
+ display: inline-block;
+ }
+ </style>
+ Patch set:
+ <span class="patchRange">
+ <select id="leftPatchSelect" on-change="_handlePatchChange">
+ <option value="PARENT"
+ selected$="[[_computeLeftSelected('PARENT', patchRange)]]">Base</option>
+ <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
+ <option value$="[[patchNum]]"
+ selected$="[[_computeLeftSelected(patchNum, patchRange)]]"
+ disabled$="[[_computeLeftDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
+ </template>
+ </select>
+ </span>
+ →
+ <span class="patchRange">
+ <select id="rightPatchSelect" on-change="_handlePatchChange">
+ <template is="dom-repeat" items="{{availablePatches}}" as="patchNum">
+ <option value$="[[patchNum]]"
+ selected$="[[_computeRightSelected(patchNum, patchRange)]]"
+ disabled$="[[_computeRightDisabled(patchNum, patchRange)]]">[[patchNum]]</option>
+ </template>
+ </select>
+ </span>
+ </template>
+ <script>
+ (function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-patch-range-select',
+
+ properties: {
+ availablePatches: Array,
+ changeNum: String,
+ patchRange: Object,
+ path: String,
+ },
+
+ _handlePatchChange: function(e) {
+ var leftPatch = this.$.leftPatchSelect.value;
+ var rightPatch = this.$.rightPatchSelect.value;
+ var rangeStr = rightPatch;
+ if (leftPatch != 'PARENT') {
+ rangeStr = leftPatch + '..' + rangeStr;
+ }
+ page.show('/c/' + this.changeNum + '/' + rangeStr + '/' + this.path);
+ },
+
+ _computeLeftSelected: function(patchNum, patchRange) {
+ return patchNum == patchRange.basePatchNum;
+ },
+
+ _computeRightSelected: function(patchNum, patchRange) {
+ return patchNum == patchRange.patchNum;
+ },
+
+ _computeLeftDisabled: function(patchNum, patchRange) {
+ return parseInt(patchNum, 10) >= parseInt(patchRange.patchNum, 10);
+ },
+
+ _computeRightDisabled: function(patchNum, patchRange) {
+ if (patchRange.basePatchNum == 'PARENT') { return false; }
+ return parseInt(patchNum, 10) <= parseInt(patchRange.basePatchNum, 10);
+ },
+ });
+ })();
+ </script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/gr-reply-dropdown.html b/polygerrit-ui/app/elements/gr-reply-dropdown.html
index 871c8e6..dd607af 100644
--- a/polygerrit-ui/app/elements/gr-reply-dropdown.html
+++ b/polygerrit-ui/app/elements/gr-reply-dropdown.html
@@ -18,6 +18,7 @@
<link rel="import" href="../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="../bower_components/iron-dropdown/iron-dropdown.html">
<link rel="import" href="../bower_components/iron-selector/iron-selector.html">
+<link rel="import" href="gr-ajax.html">
<link rel="import" href="gr-request.html">
<dom-module id="gr-reply-dropdown">
@@ -85,6 +86,9 @@
iron-selector > button.iron-selected {
background-color: #ddd;
}
+ .draftsContainer h3 {
+ margin-top: .25em;
+ }
.actionsContainer {
display: flex;
}
@@ -99,6 +103,9 @@
}
</style>
<template>
+ <gr-ajax id="draftsXHR"
+ url="[[_computeDraftsURL(changeNum)]]"
+ last-response="{{_drafts}}"></gr-ajax>
<button id="trigger" on-tap="_showPopupTapHandler">Reply</button>
<iron-dropdown id="dropdown"
vertical-align="top"
@@ -119,7 +126,7 @@
items="[[_computeLabelArray(permittedLabels)]]" as="label">
<span class="labelName">[[label]]</span>
<iron-selector data-label$="[[label]]"
- selected="[[_computeIndexOfDefaultLabelValue(labels, permittedLabels, label)]]">
+ selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]">
<template is="dom-repeat"
items="[[_computePermittedLabelValues(permittedLabels, label)]]"
as="value">
@@ -128,6 +135,13 @@
</iron-selector>
</template>
</section>
+ <section class="draftsContainer" hidden$="[[_computeHideDraftList(_drafts)]]">
+ <h3>[[_computeDraftsTitle(_drafts)]]</h3>
+ <gr-comment-list
+ comments="[[_drafts]]"
+ change-num="[[changeNum]]"
+ patch-num="[[patchNum]]"></gr-comment-list>
+ </section>
<section class="actionsContainer">
<a class="action send" href="#" on-tap="_sendTapHandler">Send</a>
<div class="danger">
@@ -161,6 +175,8 @@
labels: Object,
permittedLabels: Object,
+ _account: Object,
+ _drafts: Object,
_xhrPromise: Object, // Used for testing.
_draft: String,
},
@@ -169,7 +185,14 @@
return this.$.dropdown.opened;
},
+ ready: function() {
+ app.accountReady.then(function() {
+ this._account = app.account;
+ }.bind(this));
+ },
+
open: function() {
+ this.$.draftsXHR.generateRequest();
this.$.dropdown.open();
this.async(function() {
this.$.textarea.textarea.focus();
@@ -177,18 +200,50 @@
},
close: function() {
+ this._drafts = null;
this.$.dropdown.close();
},
+ _computeDraftsURL: function(changeNum) {
+ return '/changes/' + changeNum + '/drafts';
+ },
+
+ _computeHideDraftList: function(drafts) {
+ return Object.keys(drafts || {}).length == 0;
+ },
+
+ _computeDraftsTitle: function(drafts) {
+ var total = 0;
+ for (var file in drafts) {
+ total += drafts[file].length;
+ }
+ if (total == 0) { return ''; }
+ if (total == 1) { return '1 Draft'; }
+ if (total > 1) { return total + ' Drafts'; };
+ },
+
_computeLabelArray: function(labelsObj) {
return Object.keys(labelsObj).sort();
},
- _computeIndexOfDefaultLabelValue: function(labels, permittedLabels,
- labelName) {
+ _computeIndexOfLabelValue: function(
+ labels, permittedLabels, labelName, account) {
+ var labelValue = labels[labelName].default_value;
+
+ // Is there an existing vote for the current user? If so, use that.
+ var votes = labels[labelName];
+ if (votes.all && votes.all.length > 0) {
+ for (var i = 0; i < votes.all.length; i++) {
+ if (votes.all[i]._account_id == account._account_id) {
+ labelValue = votes.all[i].value;
+ break;
+ }
+ }
+ }
+
for (var i = 0; i < permittedLabels[labelName].length; i++) {
var val = parseInt(permittedLabels[labelName][i], 10);
- if (val == labels[labelName].default_value) {
+ if (val == labelValue) {
return i;
}
}
diff --git a/polygerrit-ui/app/scripts/app.js b/polygerrit-ui/app/scripts/app.js
index 3ceb90d..2ab2c5c 100644
--- a/polygerrit-ui/app/scripts/app.js
+++ b/polygerrit-ui/app/scripts/app.js
@@ -61,7 +61,7 @@
page.redirect('/c/' + ctx.params[0]);
});
- page('/c/:changeNum', function(data) {
+ page('/c/:changeNum/:patchNum?', function(data) {
app.route = 'gr-change-view';
app.params = data.params;
});
diff --git a/polygerrit-ui/app/scripts/ba-linkify.js b/polygerrit-ui/app/scripts/ba-linkify.js
new file mode 100644
index 0000000..26dacd6
--- /dev/null
+++ b/polygerrit-ui/app/scripts/ba-linkify.js
@@ -0,0 +1,191 @@
+/*!
+ * JavaScript Linkify - v0.3 - 6/27/2009
+ * http://benalman.com/projects/javascript-linkify/
+ *
+ * Copyright (c) 2009 "Cowboy" Ben Alman
+ * Dual licensed under the MIT and GPL licenses.
+ * http://benalman.com/about/license/
+ *
+ * Some regexps adapted from http://userscripts.org/scripts/review/7122
+ */
+
+// Script: JavaScript Linkify: Process links in text!
+//
+// *Version: 0.3, Last updated: 6/27/2009*
+//
+// Project Home - http://benalman.com/projects/javascript-linkify/
+// GitHub - http://github.com/cowboy/javascript-linkify/
+// Source - http://github.com/cowboy/javascript-linkify/raw/master/ba-linkify.js
+// (Minified) - http://github.com/cowboy/javascript-linkify/raw/master/ba-linkify.min.js (2.8kb)
+//
+// About: License
+//
+// Copyright (c) 2009 "Cowboy" Ben Alman,
+// Dual licensed under the MIT and GPL licenses.
+// http://benalman.com/about/license/
+//
+// Permission is hereby granted, free of charge, to any person
+// obtaining a copy of this software and associated documentation
+// files (the "Software"), to deal in the Software without
+// restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following
+// conditions:
+
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+
+window.linkify = (function(){
+ var
+ SCHEME = "[a-z\\d.-]+://",
+ IPV4 = "(?:(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])",
+ HOSTNAME = "(?:(?:[^\\s!@#$%^&*()_=+[\\]{}\\\\|;:'\",.<>/?]+)\\.)+",
+ TLD = "(?:ac|ad|aero|ae|af|ag|ai|al|am|an|ao|aq|arpa|ar|asia|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|biz|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|cat|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|coop|com|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|edu|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|info|int|in|io|iq|ir|is|it|je|jm|jobs|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mil|mk|ml|mm|mn|mobi|mo|mp|mq|mr|ms|mt|museum|mu|mv|mw|mx|my|mz|name|na|nc|net|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pro|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|tl|tm|tn|to|tp|travel|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g|xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp|xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw)",
+ HOST_OR_IP = "(?:" + HOSTNAME + TLD + "|" + IPV4 + ")",
+ PATH = "(?:[;/][^#?<>\\s]*)?",
+ QUERY_FRAG = "(?:\\?[^#<>\\s]*)?(?:#[^<>\\s]*)?",
+ URI1 = "\\b" + SCHEME + "[^<>\\s]+",
+ URI2 = "\\b" + HOST_OR_IP + PATH + QUERY_FRAG + "(?!\\w)",
+
+ MAILTO = "mailto:",
+ EMAIL = "(?:" + MAILTO + ")?[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@" + HOST_OR_IP + QUERY_FRAG + "(?!\\w)",
+
+ URI_RE = new RegExp( "(?:" + URI1 + "|" + URI2 + "|" + EMAIL + ")", "ig" ),
+ SCHEME_RE = new RegExp( "^" + SCHEME, "i" ),
+
+ quotes = {
+ "'": "`",
+ '>': '<',
+ ')': '(',
+ ']': '[',
+ '}': '{',
+ '»': '«',
+ '›': '‹'
+ },
+
+ default_options = {
+ callback: function( text, href ) {
+ return href ? '<a href="' + href + '" title="' + href + '">' + text + '</a>' : text;
+ },
+ punct_regexp: /(?:[!?.,:;'"]|(?:&|&)(?:lt|gt|quot|apos|raquo|laquo|rsaquo|lsaquo);)$/
+ };
+
+ return function( txt, options ) {
+ options = options || {};
+
+ // Temp variables.
+ var arr,
+ i,
+ link,
+ href,
+
+ // Output HTML.
+ html = '',
+
+ // Store text / link parts, in order, for re-combination.
+ parts = [],
+
+ // Used for keeping track of indices in the text.
+ idx_prev,
+ idx_last,
+ idx,
+ link_last,
+
+ // Used for trimming trailing punctuation and quotes from links.
+ matches_begin,
+ matches_end,
+ quote_begin,
+ quote_end;
+
+ // Initialize options.
+ for ( i in default_options ) {
+ if ( options[ i ] === undefined ) {
+ options[ i ] = default_options[ i ];
+ }
+ }
+
+ // Find links.
+ while ( arr = URI_RE.exec( txt ) ) {
+
+ link = arr[0];
+ idx_last = URI_RE.lastIndex;
+ idx = idx_last - link.length;
+
+ // Not a link if preceded by certain characters.
+ if ( /[\/:]/.test( txt.charAt( idx - 1 ) ) ) {
+ continue;
+ }
+
+ // Trim trailing punctuation.
+ do {
+ // If no changes are made, we don't want to loop forever!
+ link_last = link;
+
+ quote_end = link.substr( -1 )
+ quote_begin = quotes[ quote_end ];
+
+ // Ending quote character?
+ if ( quote_begin ) {
+ matches_begin = link.match( new RegExp( '\\' + quote_begin + '(?!$)', 'g' ) );
+ matches_end = link.match( new RegExp( '\\' + quote_end, 'g' ) );
+
+ // If quotes are unbalanced, remove trailing quote character.
+ if ( ( matches_begin ? matches_begin.length : 0 ) < ( matches_end ? matches_end.length : 0 ) ) {
+ link = link.substr( 0, link.length - 1 );
+ idx_last--;
+ }
+ }
+
+ // Ending non-quote punctuation character?
+ if ( options.punct_regexp ) {
+ link = link.replace( options.punct_regexp, function(a){
+ idx_last -= a.length;
+ return '';
+ });
+ }
+ } while ( link.length && link !== link_last );
+
+ href = link;
+
+ // Add appropriate protocol to naked links.
+ if ( !SCHEME_RE.test( href ) ) {
+ href = ( href.indexOf( '@' ) !== -1 ? ( !href.indexOf( MAILTO ) ? '' : MAILTO )
+ : !href.indexOf( 'irc.' ) ? 'irc://'
+ : !href.indexOf( 'ftp.' ) ? 'ftp://'
+ : 'http://' )
+ + href;
+ }
+
+ // Push preceding non-link text onto the array.
+ if ( idx_prev != idx ) {
+ parts.push([ txt.slice( idx_prev, idx ) ]);
+ idx_prev = idx_last;
+ }
+
+ // Push massaged link onto the array
+ parts.push([ link, href ]);
+ };
+
+ // Push remaining non-link text onto the array.
+ parts.push([ txt.substr( idx_prev ) ]);
+
+ // Process the array items.
+ for ( i = 0; i < parts.length; i++ ) {
+ html += options.callback.apply( window, parts[i] );
+ }
+
+ // In case of catastrophic failure, return the original text;
+ return html || txt;
+ };
+
+})();
\ No newline at end of file
diff --git a/polygerrit-ui/app/scripts/link-text-parser.js b/polygerrit-ui/app/scripts/link-text-parser.js
new file mode 100644
index 0000000..e80d19b
--- /dev/null
+++ b/polygerrit-ui/app/scripts/link-text-parser.js
@@ -0,0 +1,66 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+'use strict';
+
+function GrLinkTextParser(linkConfig, callback) {
+ this.linkConfig = linkConfig;
+ this.callback = callback;
+ Object.preventExtensions(this);
+}
+
+GrLinkTextParser.SUB_REGEX = /\$(\d+)/;
+
+GrLinkTextParser.prototype.addText = function(text, href) {
+ if (!text) {
+ return;
+ }
+ this.callback(text, href);
+};
+
+GrLinkTextParser.prototype.parse = function(text) {
+ linkify(text, {
+ callback: this.parseChunk.bind(this)
+ });
+};
+
+GrLinkTextParser.prototype.parseChunk = function(text, href) {
+ if (href) {
+ this.addText(text, href);
+ } else {
+ this.parseLinks(text, this.linkConfig);
+ }
+};
+
+GrLinkTextParser.prototype.parseLinks = function(text, patterns) {
+ for (var p in patterns) {
+ var pattern = new RegExp(patterns[p].match);
+ var match = text.match(pattern);
+ if (!match) { continue; }
+
+ var link = patterns[p].link;
+ var subMatch = link.match(GrLinkTextParser.SUB_REGEX);
+ link = link.replace(GrLinkTextParser.SUB_REGEX, match[subMatch[1]]);
+
+ // PolyGerrit doesn't use hash-based navigation like GWT. Account for this.
+ if (link[0] == '#') {
+ link = link.substr(1);
+ }
+ var before = text.substr(0, match.index);
+ this.addText(before);
+ text = text.substr(match.index + match[0].length);
+ this.addText(match[0], link);
+ }
+ this.addText(text);
+};
diff --git a/polygerrit-ui/app/test/gr-change-list-item-test.html b/polygerrit-ui/app/test/gr-change-list-item-test.html
index bca44a9..c0b1187 100644
--- a/polygerrit-ui/app/test/gr-change-list-item-test.html
+++ b/polygerrit-ui/app/test/gr-change-list-item-test.html
@@ -72,9 +72,13 @@
assert.equal(element._computeVerifiedLabel({approved: true}), '✓');
- assert.equal(element._computeOwnerLink('andybons+gerrit@gmail.com'),
+ assert.equal(element._computeOwnerLink(
+ { email: 'andybons+gerrit@gmail.com' }),
'/q/owner:andybons%2Bgerrit%40gmail.com+status:open');
+ assert.equal(element._computeOwnerLink({ _account_id: 42 }),
+ '/q/owner:42+status:open');
+
assert.equal(element._computeOwnerTitle(
{
name: 'Andrew Bonventre',
@@ -82,7 +86,8 @@
}),
'Andrew Bonventre <andybons+gerrit@gmail.com>');
- // TODO(andybons): _computeProjectURL once it's not a constant.
+ assert.equal(element._computeProjectURL('combustible-stuff'),
+ '/projects/combustible-stuff,dashboards/default');
assert.equal(element._computeProjectBranchURL(
'combustible-stuff', 'lemons'),
diff --git a/polygerrit-ui/app/test/gr-change-view-test.html b/polygerrit-ui/app/test/gr-change-view-test.html
index 4cf1d4c..084d4a8 100644
--- a/polygerrit-ui/app/test/gr-change-view-test.html
+++ b/polygerrit-ui/app/test/gr-change-view-test.html
@@ -37,9 +37,27 @@
<script>
suite('gr-change-view tests', function() {
var element;
+ var server;
setup(function() {
element = fixture('basic');
+ element.$.configXHR.auto = false;
+
+ server = sinon.fakeServer.create();
+ // Eat any requests made by elements in this suite.
+ server.respondWith(
+ 'GET',
+ /\/changes\/(.*)/,
+ [
+ 200,
+ { 'Content-Type': 'application/json' },
+ ')]}\'\n{}',
+ ]
+ );
+ });
+
+ teardown(function() {
+ server.restore();
});
test('keyboard shortcuts', function() {
@@ -57,5 +75,49 @@
assert.isFalse(dropdownEl.opened);
});
+ test('patch num change', function(done) {
+ element._changeNum = '42';
+ element._patchNum = 2;
+ element._change = {
+ revisions: {
+ rev2: { _number: 2 },
+ rev1: { _number: 1 },
+ rev3: { _number: 3 },
+ },
+ current_revision: 'rev3',
+ };
+ flushAsynchronousOperations();
+ var selectEl = element.$$('.header select');
+ assert.ok(selectEl);
+ var optionEls =
+ Polymer.dom(element.root).querySelectorAll('.header option');
+ assert.equal(optionEls.length, 3);
+ assert.isFalse(
+ element.$$('.header option[value="1"]').hasAttribute('selected'));
+ assert.isTrue(
+ element.$$('.header option[value="2"]').hasAttribute('selected'));
+ assert.isFalse(
+ element.$$('.header option[value="3"]').hasAttribute('selected'));
+
+ var showStub = sinon.stub(page, 'show');
+
+ var numEvents = 0;
+ selectEl.addEventListener('change', function(e) {
+ numEvents++;
+ if (numEvents == 1) {
+ assert(showStub.lastCall.calledWithExactly('/c/42/1'),
+ 'Should navigate to /c/42/1');
+ selectEl.value = '3';
+ element.fire('change', {}, {node: selectEl});
+ } else if (numEvents == 2) {
+ assert(showStub.lastCall.calledWithExactly('/c/42'),
+ 'Should navigate to /c/42');
+ showStub.restore();
+ done();
+ }
+ });
+ selectEl.value = '1';
+ element.fire('change', {}, {node: selectEl});
+ });
});
</script>
diff --git a/polygerrit-ui/app/test/gr-diff-side-test.html b/polygerrit-ui/app/test/gr-diff-side-test.html
new file mode 100644
index 0000000..36eed2f
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-diff-side-test.html
@@ -0,0 +1,187 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-diff-side</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.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="../elements/gr-diff-side.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-diff-side></gr-diff-side>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff-side tests', function() {
+ var element;
+
+ function isVisibleInWindow(el) {
+ var rect = el.getBoundingClientRect();
+ return rect.top >= 0 && rect.left >= 0 &&
+ rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
+ }
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('comments', function() {
+ assert.isFalse(element.$$('.container').classList.contains('canComment'));
+ element.canComment = true;
+ assert.isTrue(element.$$('.container').classList.contains('canComment'));
+ // TODO(andybons): Check for comment creation events firing/not firing
+ // when implemented.
+ });
+
+ test('scroll to line', function() {
+ var content = [];
+ for (var i = 0; i < 300; i++) {
+ content.push({
+ type: 'CODE',
+ content: 'All work and no play makes Jack a dull boy',
+ numLines: 1,
+ lineNum: i + 1,
+ highlight: false,
+ intraline: [],
+ });
+ }
+ element.content = content;
+
+ window.scrollTo(0, 0);
+ element.scrollToLine(-12849);
+ assert.equal(window.scrollY, 0);
+ element.scrollToLine('sup');
+ assert.equal(window.scrollY, 0);
+ var lineEl = element.$$('.numbers .lineNum[data-line-num="150"]');
+ assert.ok(lineEl);
+ element.scrollToLine(150);
+ assert.isAbove(window.scrollY, 0);
+ assert.isTrue(isVisibleInWindow(lineEl), 'element should be visible');
+ });
+
+ test('intraline highlights', function() {
+ var content = ' <gr-linked-text content="' +
+ '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>';
+ var html = util.escapeHTML(content);
+ var highlights = [
+ { startIndex: 0, endIndex: 33, },
+ { startIndex: 75 },
+ ];
+ assert.equal(
+ content.slice(highlights[0].startIndex, highlights[0].endIndex),
+ ' <gr-linked-text content="');
+ assert.equal(content.slice(highlights[1].startIndex),
+ '"></gr-linked-text>');
+ var result = element._addIntralineHighlights(content, html, highlights);
+ var expected = element._highlightStartTag +
+ ' <gr-linked-text content="' +
+ element._highlightEndTag +
+ '[[_computeCurrentRevisionMessage(change)]]' +
+ element._highlightStartTag +
+ '"></gr-linked-text>' +
+ element._highlightEndTag;
+ assert.equal(result, expected);
+ });
+
+ test('newlines', function() {
+ element.width = 80;
+ var content = [{
+ type: 'CODE',
+ content: 'A'.repeat(50),
+ numLines: 1,
+ lineNum: 1,
+ highlight: false,
+ intraline: [],
+ }];
+ element.content = content;
+
+ var lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
+ assert.ok(lineEl);
+ var contentEl = element.$$('.content .code[data-line-num="1"]');
+ assert.ok(contentEl);
+ assert.equal(contentEl.innerHTML, 'A'.repeat(50));
+
+ content = [{
+ type: 'CODE',
+ content: 'A'.repeat(100),
+ numLines: 2,
+ lineNum: 1,
+ highlight: false,
+ intraline: [],
+ }];
+ element.content = content;
+
+ lineEl = element.$$('.numbers .lineNum[data-line-num="1"]');
+ assert.ok(lineEl);
+ contentEl = element.$$('.content .code[data-line-num="1"]');
+ assert.ok(contentEl);
+ assert.equal(contentEl.innerHTML,
+ 'A'.repeat(80) + element._lineFeedHTML +
+ 'A'.repeat(20) + element._lineFeedHTML);
+
+ });
+
+ test('diff context', function() {
+ var content = [
+ {type: 'CODE', hidden: true, content: '<!DOCTYPE html>'},
+ {type: 'CODE', hidden: true, content: '<meta charset="utf-8">'},
+ {type: 'CODE', hidden: true, content: '<title>My great page</title>'},
+ {type: 'CODE', hidden: true, content: '<style>'},
+ {type: 'CODE', hidden: true, content: ' *,'},
+ {type: 'CODE', hidden: true, content: ' *:before,'},
+ {type: 'CODE', hidden: true, content: ' *:after {'},
+ {type: 'CODE', hidden: true, content: ' box-sizing: border-box;'},
+ {type: 'CONTEXT_CONTROL', numLines: 8, start: 0, end: 8},
+ {type: 'CODE', hidden: false, content: ' }'},
+ ];
+ element.content = content;
+
+ // Only the context elements and the following code line elements should
+ // be present in the DOM.
+ var contextEls =
+ Polymer.dom(element.root).querySelectorAll('.contextControl');
+ assert.equal(contextEls.length, 2);
+ var codeLineEls =
+ Polymer.dom(element.root).querySelectorAll('.lineNum, .code');
+ assert.equal(codeLineEls.length, 2);
+
+ for (var i = 0; i <= 8; i++) {
+ element.content[i].hidden = false;
+ }
+ element.renderLineIndexRange(0, 8);
+ element.hideElementsWithIndex(8);
+
+ contextEls =
+ Polymer.dom(element.root).querySelectorAll('.contextControl');
+ for (var i = 0; i < contextEls.length; i++) {
+ assert.isTrue(contextEls[i].hasAttribute('hidden'));
+ }
+
+ codeLineEls =
+ Polymer.dom(element.root).querySelectorAll('.lineNum, .code');
+
+ // Nine lines should now be present in the DOM.
+ assert.equal(codeLineEls.length, 9 * 2);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-diff-test.html b/polygerrit-ui/app/test/gr-diff-test.html
new file mode 100644
index 0000000..d1e5ad9
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-diff-test.html
@@ -0,0 +1,344 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<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/web-component-tester/browser.js"></script>
+<script src="../scripts/changes.js"></script>
+<script src="../scripts/fake-app.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="../elements/gr-diff.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-diff auto></gr-diff>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-diff tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+ element.changeNum = 42;
+ element.path = 'sieve.go';
+
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'GET',
+ /\/changes\/42\/revisions\/(1|2)\/files\/sieve\.go\/diff(.*)/,
+ [
+ 200,
+ { 'Content-Type': 'application/json' },
+ ')]}\'\n' +
+ JSON.stringify({
+ change_type: 'MODIFIED',
+ content: [
+ {ab: ['doin some codez and stuffs']},
+ ]
+ }),
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/1/comments',
+ [
+ 200,
+ { 'Content-Type': 'application/json' },
+ ')]}\'\n' +
+ JSON.stringify({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ author: {
+ _account_id: 1000000,
+ name: 'Andrew Bonventre',
+ email: 'andybons@gmail.com',
+ },
+ id: '9af53d3f_5f2b8b82',
+ line: 1,
+ message: 'this isn’t quite right',
+ updated: '2015-12-10 02:50:21.627000000',
+ },
+ {
+ author: {
+ _account_id: 1000000,
+ name: 'Andrew Bonventre',
+ email: 'andybons@gmail.com',
+ },
+ id: '9af53d3f_bf1cd76b',
+ line: 1,
+ side: 'PARENT',
+ message: 'how did this work in the first place?',
+ updated: '2015-12-10 00:08:42.255000000',
+ },
+ ],
+ }),
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/2/comments',
+ [
+ 200,
+ { 'Content-Type': 'application/json' },
+ ')]}\'\n' +
+ JSON.stringify({
+ '/COMMIT_MSG': [],
+ 'sieve.go': [
+ {
+ author: {
+ _account_id: 1010008,
+ name: 'Dave Borowitz',
+ email: 'dborowitz@google.com',
+ },
+ id: '001a2067_f30f3048',
+ line: 17,
+ message: 'What on earth are you thinking, here?',
+ updated: '2015-12-12 02:51:37.973000000',
+ },
+ {
+ author: {
+ _account_id: 1010008,
+ name: 'Dave Borowitz',
+ email: 'dborowitz@google.com',
+ },
+ id: '001a2067_f6b1b1c8',
+ in_reply_to: '9af53d3f_bf1cd76b',
+ line: 1,
+ side: 'PARENT',
+ message: 'Yeah not sure how this worked either?',
+ updated: '2015-12-12 02:51:37.973000000',
+ },
+ {
+ author: {
+ _account_id: 1000000,
+ name: 'Andrew Bonventre',
+ email: 'andybons@gmail.com',
+ },
+ id: 'a0407443_30dfe8fb',
+ in_reply_to: '001a2067_f30f3048',
+ line: 17,
+ message: '¯\\_(ツ)_/¯',
+ updated: '2015-12-12 18:50:21.627000000',
+ },
+ ],
+ }),
+ ]
+ );
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ test('comments with parent', function(done) {
+ element.patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: 1,
+ };
+
+ server.respond();
+
+ element._diffRequestsPromise.then(function() {
+ assert.equal(element._baseComments.length, 1);
+ assert.equal(element._comments.length, 1);
+ assert.equal(element._baseDrafts.length, 0);
+ assert.equal(element._drafts.length, 0);
+ done();
+ });
+ });
+
+ test('comments between two patches', function(done) {
+ element.patchRange = {
+ basePatchNum: 1,
+ patchNum: 2,
+ };
+
+ server.respond();
+
+ element._diffRequestsPromise.then(function() {
+ assert.equal(element._baseComments.length, 1);
+ assert.equal(element._comments.length, 2);
+ assert.equal(element._baseDrafts.length, 0);
+ assert.equal(element._drafts.length, 0);
+ done();
+ });
+ });
+
+ test('intraline normalization', function() {
+ // The content and highlights are in the format returned by the Gerrit
+ // REST API.
+ var content = [
+ ' <section class="summary">',
+ ' <gr-linked-text content="' +
+ '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>',
+ ' </section>',
+ ];
+ var highlights = [
+ [31, 34], [42, 26]
+ ];
+ var results = element._normalizeIntralineHighlights(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 31,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 0,
+ endIndex: 33,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 75,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 0,
+ endIndex: 6,
+ }
+ ]);
+
+ content = [
+ ' this._path = value.path;',
+ '',
+ ' // When navigating away from the page, there is a possibility that the',
+ ' // patch number is no longer a part of the URL (say when navigating to',
+ ' // the top-level change info view) and therefore undefined in `params`.',
+ ' if (!this._patchRange.patchNum) {',
+ ];
+ highlights = [
+ [14, 17],
+ [11, 70],
+ [12, 67],
+ [12, 67],
+ [14, 29],
+ ];
+ results = element._normalizeIntralineHighlights(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 14,
+ endIndex: 31,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 8,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 3,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 4,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 5,
+ startIndex: 12,
+ endIndex: 41,
+ }
+ ]);
+ });
+
+ test('context', function() {
+ element._context = 3;
+ element._diffResponse = {
+ content: [
+ {
+ ab: [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ '<title>My great page</title>',
+ '<style>',
+ ' *,',
+ ' *:before,',
+ ' *:after {',
+ ' box-sizing: border-box;',
+ ' }',
+ '</style>',
+ '<header>',
+ ]
+ },
+ {
+ a: [
+ ' Welcome ',
+ ' to the wooorld of tomorrow!',
+ ],
+ b: [
+ ' Hello, world!',
+ ],
+ },
+ {
+ ab: [
+ '</header>',
+ '<body>',
+ 'Leela: This is the only place the ship can’t hear us, so ',
+ 'everyone pretend to shower.',
+ 'Fry: Same as every day. Got it.',
+ ]
+ },
+ ]
+ };
+ element._processContent();
+
+ // First eight lines should be hidden on both sides.
+ for (var i = 0; i < 8; i++) {
+ assert.isTrue(element._diff.leftSide[i].hidden);
+ assert.isTrue(element._diff.rightSide[i].hidden);
+ }
+ // A context control should be at index 8 on both sides.
+ var leftContext = element._diff.leftSide[8];
+ var rightContext = element._diff.rightSide[8];
+ assert.deepEqual(leftContext, rightContext);
+ assert.equal(leftContext.numLines, 8);
+ assert.equal(leftContext.start, 0);
+ assert.equal(leftContext.end, 8);
+
+ // Line indices 9-16 should be shown.
+ for (var i = 9; i <= 16; i++) {
+ // notOk (falsy) because the `hidden` attribute may not be present.
+ assert.notOk(element._diff.leftSide[i].hidden);
+ assert.notOk(element._diff.rightSide[i].hidden);
+ }
+
+ // Lines at indices 17 and 18 should be hidden.
+ assert.isTrue(element._diff.leftSide[17].hidden);
+ assert.isTrue(element._diff.rightSide[17].hidden);
+ assert.isTrue(element._diff.leftSide[18].hidden);
+ assert.isTrue(element._diff.rightSide[18].hidden);
+
+ // Context control at index 19.
+ leftContext = element._diff.leftSide[19];
+ rightContext = element._diff.rightSide[19];
+ assert.deepEqual(leftContext, rightContext);
+ assert.equal(leftContext.numLines, 2);
+ assert.equal(leftContext.start, 17);
+ assert.equal(leftContext.end, 19);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-diff-view-test.html b/polygerrit-ui/app/test/gr-diff-view-test.html
index 9e2b13f..5e9e355 100644
--- a/polygerrit-ui/app/test/gr-diff-view-test.html
+++ b/polygerrit-ui/app/test/gr-diff-view-test.html
@@ -22,6 +22,7 @@
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../../bower_components/page/page.js"></script>
<script src="../scripts/changes.js"></script>
+<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
@@ -29,266 +30,26 @@
<test-fixture id="basic">
<template>
- <gr-diff-view ruler-width="0"></gr-diff-view>
- </template>
-</test-fixture>
-
-<test-fixture id="comments">
- <template>
- <gr-diff-view></gr-diff-view>
- </template>
-</test-fixture>
-
-<test-fixture id="manylines">
- <template>
<gr-diff-view></gr-diff-view>
</template>
</test-fixture>
<script>
- // Original diff:
- // Left side (side A):
- // 1: if i < 5 { // "comments" &= \'fun\' && true
- // 2: println("i is less than five")
- // 3: }
- // 4:
- // 5:
- // 6: // Comment
- // 7: foo
- // 8: bar
- // 9: Bad news: combustible lemons failed.
- //
- // Right side (side B):
- // 1: if i < 5 { // "comments" &= \'fun\' && true
- // 2: println("i is less than five")
- // 3: }
- // 4:
- // 5:
- // 6: // Comment
- // 7: baz
- // 8: Bad news: combustible lemons failed.
- //
- var diffContent = [
- {
- ab: [
- 'if i < 5 { // "comments" &= \'fun\' && true',
- ' println("i is less than five")',
- '}',
- '',
- '',
- '// Comment'
- ]
- },
- {
- a: [
- 'foo',
- 'bar'
- ],
- b: [
- 'baz',
- ]
- },
- {
- ab: [
- 'Bad news: combustible lemons failed.'
- ]
- }
- ];
-
- suite('<gr-diff-view>', function() {
+ suite('gr-diff-view tests', function() {
var element;
setup(function() {
element = fixture('basic');
- element._renderDiff({content: diffContent}, [], [], [], []);
- flushAsynchronousOperations();
- });
-
- test('ab content is the same for left and right sides', function() {
- for (var i = 0; i < diffContent[0].ab.length; i++) {
- var leftEls = Polymer.dom(element.root).querySelectorAll(
- '#leftDiffContent .content[data-line-num="' + (i + 1) + '"]');
- assert.equal(leftEls.length, 1);
- var rightEls = Polymer.dom(element.root).querySelectorAll(
- '#rightDiffContent .content[data-line-num="' + (i + 1) + '"]');
- assert.equal(rightEls.length, 1);
- assert.equal(leftEls[0].textContent, rightEls[0].textContent);
- }
- });
-
- test('all line number and content elements have same (non-zero) height',
- function() {
- var els = Polymer.dom(element.root).querySelectorAll('.lineNum, .content');
- assert.isAbove(els.length, 0);
- var offsetHeight = els.length > 0 && els[0].offsetHeight;
- assert.isAbove(offsetHeight, 0);
- for (var i = 0; i < els.length; i++) {
- assert.equal(offsetHeight, els[i].offsetHeight);
- }
- });
-
- test('content is properly escaped', function() {
- var firstLineEls = Polymer.dom(element.root).querySelectorAll(
- '#leftDiffContent .content[data-line-num="1"], ' +
- '#rightDiffContent .content[data-line-num="1"]');
- assert.equal(2, firstLineEls.length);
- for (var i = 0; i < firstLineEls.length; i++) {
- assert.equal(firstLineEls[i].innerHTML,
- 'if i < 5 { // "comments" &= \'fun\' && true');
- }
- });
-
- test('content and line numbers are correct for a/b edit', function() {
- assert.equal(element.$$(
- '#leftDiffContent .content[data-line-num="7"]').textContent, 'foo');
- assert.equal(element.$$(
- '#leftDiffContent .content[data-line-num="8"]').textContent, 'bar');
- assert.equal(element.$$(
- '#rightDiffContent .content[data-line-num="7"]').textContent, 'baz');
- assert.equal(element.$$(
- '#leftDiffContent .content[data-line-num="9"]').textContent,
- element.$$(
- '#rightDiffContent .content[data-line-num="8"]').textContent);
- });
-
- test('ruler width changes are applied correctly', function() {
- assert.equal(element.rulerWidth, 0);
- assert.equal(Polymer.dom(element.root).querySelectorAll('.ruler').length,
- 0);
- element.rulerWidth = 80;
- flushAsynchronousOperations();
- var els = Polymer.dom(element.root).querySelectorAll('.ruler');
- assert.isAbove(els.length, 0);
- for (var i = 0; i < els.length; i++) {
- assert.equal(els[i].style.left, '80ch');
- }
- element.rulerWidth = 0;
- flushAsynchronousOperations();
- assert.equal(Polymer.dom(element.root).querySelectorAll('.ruler').length,
- 0);
- element.rulerWidth = 100;
- flushAsynchronousOperations();
- els = Polymer.dom(element.root).querySelectorAll('.ruler');
- assert.isAbove(els.length, 0);
- for (var i = 0; i < els.length; i++) {
- assert.equal(els[i].style.left, '100ch');
- }
- });
- });
-
- suite('comments and drafts', function() {
- var element;
-
- setup(function(done) {
- element = fixture('comments');
- element._patchNum = 1;
- element._renderDiff({content: diffContent}, [], [
- {
- id: 'file_comment',
- message: 'this is a file comment about the meaninglessness of life',
- author: {
- name: 'GLaDOS'
- }
- },
- {
- id: 'all_the_lemons',
- line: 8,
- message: 'MAKE LIFE TAKE THE LEMONS BACK',
- author: {
- name: 'Cave Johnson',
- }
- }
- ], [], []);
-
- // On WebKit and Gecko, flushAsynchronousOperations isn't enough to allow
- // the thread filler elements to properly render. Spin the runloop.
- element.async(function() {
- done();
- }, 1);
- });
-
- test('comment threads are rendered correctly', function() {
- var threadEls = Polymer.dom(element.root).querySelectorAll(
- 'gr-diff-comment-thread[data-thread-id="thread-1-8"]');
- assert.equal(threadEls.length, 1);
- var fillerEls = Polymer.dom(element.root).querySelectorAll(
- '.js-threadFiller[data-thread-id="thread-1-8"]');
- assert.equal(fillerEls.length, 3);
-
- threadEls = Polymer.dom(element.root).querySelectorAll(
- 'gr-diff-comment-thread[data-thread-id="thread-1-FILE"]');
- assert.equal(threadEls.length, 1);
- fillerEls = Polymer.dom(element.root).querySelectorAll(
- '.js-threadFiller[data-thread-id="thread-1-FILE"]');
- assert.equal(fillerEls.length, 3);
- });
-
- test('tapping a line with an existing thread', function() {
- var threadEls = Polymer.dom(element.root).querySelectorAll(
- 'gr-diff-comment-thread[data-line-num="8"][data-patch-num="1"]');
- assert.equal(threadEls.length, 1);
- var lineEl = element.$$(
- '.lineNum[data-line-num="8"][data-patch-num="1"]');
- assert.ok(lineEl);
- MockInteractions.tap(lineEl);
- threadEls = Polymer.dom(element.root).querySelectorAll(
- 'gr-diff-comment-thread[data-line-num="8"][data-patch-num="1"]');
- assert.equal(threadEls.length, 1);
- });
-
- test('creating a draft', function() {
- var threadEls = Polymer.dom(element.root).querySelectorAll(
- 'gr-diff-comment-thread[data-line-num="5"][data-patch-num="1"]');
- assert.equal(threadEls.length, 0);
- var lineEl = element.$$(
- '.lineNum[data-line-num="5"][data-patch-num="1"]');
- assert.ok(lineEl);
- MockInteractions.tap(lineEl);
- threadEls = Polymer.dom(element.root).querySelectorAll(
- 'gr-diff-comment-thread[data-line-num="5"][data-patch-num="1"]');
- assert.equal(threadEls.length, 1);
- });
- });
-
- suite('long diffs', function() {
- var element;
-
- setup(function() {
- element = fixture('manylines');
- var longDiffContent = [{ ab: [] }];
- for (var i = 0; i < 300; i++) {
- longDiffContent[0].ab.push('');
- }
- element._renderDiff({content: longDiffContent}, [], [], [], []);
- });
-
- function isVisibleInWindow(el) {
- var rect = el.getBoundingClientRect();
- return rect.top >= 0 && rect.left >= 0 &&
- rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
- }
-
- test('jump to line', function() {
- window.scrollTo(0, 0);
- element._jumpToLine(-12849);
- assert.equal(window.scrollY, 0);
- element._jumpToLine('sup');
- assert.equal(window.scrollY, 0);
- // Use the left line numbers in this case because the viewport may be too
- // thin for the right line number element to be visible. Since the content
- // is the same for both sides, it should not make a difference.
- var lineEl =
- element.$$('.diffNumbers.left .lineNum[data-line-num="150"]');
- assert.isFalse(isVisibleInWindow(lineEl),
- 'element should not be visible');
- element._jumpToLine(150);
- assert.isAbove(window.scrollY, 0);
- assert.isTrue(isVisibleInWindow(lineEl), 'element should be visible');
+ element.$.changeDetailXHR.auto = false;
+ element.$.filesXHR.auto = false;
+ element.$.diff.auto = false;
});
test('keyboard shortcuts', function() {
element._changeNum = '42';
- element._patchNum = '10';
+ element._patchRange = {
+ patchNum: '10',
+ };
element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
element._path = 'glados.txt';
@@ -349,5 +110,25 @@
}, 1);
}
});
+
+ test('jump to file dropdown', function() {
+ element._changeNum = '42';
+ element._patchRange = {
+ patchNum: '10',
+ };
+ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md'];
+ element._path = 'glados.txt';
+ flushAsynchronousOperations();
+ var linkEls =
+ Polymer.dom(element.root).querySelectorAll('.dropdown-content > a');
+ assert.equal(linkEls.length, 3);
+ assert.isFalse(linkEls[0].hasAttribute('selected'));
+ assert.isTrue(linkEls[1].hasAttribute('selected'));
+ assert.isFalse(linkEls[2].hasAttribute('selected'));
+ assert.equal(linkEls[0].getAttribute('data-key-nav'), '[');
+ assert.equal(linkEls[1].getAttribute('data-key-nav'), '');
+ assert.equal(linkEls[2].getAttribute('data-key-nav'), ']');
+ });
+
});
</script>
diff --git a/polygerrit-ui/app/test/gr-file-list-test.html b/polygerrit-ui/app/test/gr-file-list-test.html
new file mode 100644
index 0000000..f474f15
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-file-list-test.html
@@ -0,0 +1,126 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<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/web-component-tester/browser.js"></script>
+<script src="../../bower_components/page/page.js"></script>
+<script src="../scripts/changes.js"></script>
+<script src="../scripts/fake-app.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="../elements/gr-file-list.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-file-list></gr-file-list>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-file-list tests', function() {
+ var element;
+ var server;
+
+ setup(function() {
+ element = fixture('basic');
+ server = sinon.fakeServer.create();
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/1/files',
+ [
+ 200,
+ { 'Content-Type': 'application/json' },
+ ')]}\'\n' +
+ JSON.stringify({
+ '/COMMIT_MSG': {
+ status: 'A',
+ lines_inserted: 9,
+ size_delta: 317,
+ size: 317
+ },
+ 'myfile.txt': {
+ lines_inserted: 35,
+ size_delta: 1146,
+ size: 1167
+ }
+ }),
+ ]
+ );
+ server.respondWith(
+ 'GET',
+ '/changes/42/revisions/2/files',
+ [
+ 200,
+ { 'Content-Type': 'application/json' },
+ ')]}\'\n' +
+ JSON.stringify({
+ '/COMMIT_MSG': {
+ status: 'A',
+ lines_inserted: 9,
+ size_delta: 317,
+ size: 317
+ },
+ 'myfile.txt': {
+ lines_inserted: 35,
+ size_delta: 1146,
+ size: 1167
+ },
+ 'file_added_in_rev2.txt': {
+ lines_inserted: 98,
+ size_delta: 234,
+ size: 136
+ }
+ }),
+ ]
+ );
+ });
+
+ teardown(function() {
+ server.restore();
+ });
+
+ test('requests', function(done) {
+ element.changeNum = '42';
+ element.patchNum = '1';
+ element.reload();
+ server.respond();
+
+ element.async(function() {
+ var filenames = element.files.map(function(f) {
+ return f.__path;
+ });
+ assert.deepEqual(filenames, ['/COMMIT_MSG', 'myfile.txt']);
+
+ element.patchNum = '2';
+ element.reload();
+ server.respond();
+ element.async(function() {
+ filenames = element.files.map(function(f) {
+ return f.__path;
+ });
+ assert.deepEqual(filenames,
+ ['/COMMIT_MSG', 'file_added_in_rev2.txt', 'myfile.txt']);
+ done();
+ }, 1);
+ }, 1)
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-linked-text-test.html b/polygerrit-ui/app/test/gr-linked-text-test.html
new file mode 100644
index 0000000..727e018
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-linked-text-test.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-linked-text</title>
+
+<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="../elements/gr-linked-text.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-linked-text>
+ <div id="output"></div>
+ </gr-linked-text>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-linked-text tests', function() {
+ var element;
+ var testStrings;
+
+ setup(function() {
+ element = fixture('basic');
+ element.config = {
+ ph: {
+ match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)',
+ link: 'https://code.google.com/p/gerrit/issues/detail?id=$2'
+ },
+ changeid: {
+ 'match': '(I[0-9a-f]{8,40})',
+ 'link': '#/q/$1'
+ }
+ };
+ testStrings = [
+ {
+ 'text': 'https://code.google.com/p/gerrit/issues/detail?id=3650',
+ 'linkedText': '<a href="https://code.google.com/p/gerrit/issues/detail?id=3650" target="_blank">https://code.google.com/p/gerrit/issues/detail?id=3650</a>'
+ },
+ {
+ 'text': 'Issue 3650',
+ 'linkedText': '<a href="https://code.google.com/p/gerrit/issues/detail?id=3650" target="_blank">Issue 3650</a>'
+ },
+ {
+ 'text': 'bug 3650',
+ 'linkedText': '<a href="https://code.google.com/p/gerrit/issues/detail?id=3650" target="_blank">bug 3650</a>'
+ },
+ {
+ 'text': 'Change-Id: I11d6a37f5e9b5df0486f6c922d8836dfa780e03e',
+ 'linkedText': 'Change-Id: <a href="/q/I11d6a37f5e9b5df0486f6c922d8836dfa780e03e" target="_blank">I11d6a37f5e9b5df0486f6c922d8836dfa780e03e</a>'
+ }
+ ];
+ });
+
+ test('URL pattern was parsed and linked.', function() {
+ // Reguar inline link.
+ element._contentChanged(testStrings[0].text, element.config);
+ assert.equal(element.$.output.innerHTML, testStrings[0].linkedText);
+ });
+
+ test('Bug pattern was parsed and linked', function() {
+ // "Issue/Bug" pattern.
+ element._contentChanged(testStrings[1].text, element.config);
+ assert.equal(element.$.output.innerHTML, testStrings[1].linkedText);
+
+ element._contentChanged(testStrings[2].text, element.config);
+ assert.equal(element.$.output.innerHTML, testStrings[2].linkedText);
+ });
+
+ test('Change-Id pattern was parsed and linked', function() {
+ // "Change-Id:" pattern.
+ element._contentChanged(testStrings[3].text, element.config);
+ assert.equal(element.$.output.innerHTML, testStrings[3].linkedText);
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-patch-range-select-test.html b/polygerrit-ui/app/test/gr-patch-range-select-test.html
new file mode 100644
index 0000000..c2b19e7
--- /dev/null
+++ b/polygerrit-ui/app/test/gr-patch-range-select-test.html
@@ -0,0 +1,93 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<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/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="../elements/gr-patch-range-select.html">
+
+<test-fixture id="basic">
+ <template>
+ <gr-patch-range-select auto></gr-patch-range-select>
+ </template>
+</test-fixture>
+
+<script>
+ suite('gr-patch-range-select tests', function() {
+ var element;
+
+ setup(function() {
+ element = fixture('basic');
+ });
+
+ test('enabled/disabled options', function() {
+ var patchRange = {
+ basePatchNum: 'PARENT',
+ patchNum: '3',
+ };
+ ['1', '2', '3'].forEach(function(patchNum) {
+ assert.isFalse(element._computeRightDisabled(patchNum, patchRange));
+ });
+ ['PARENT', '1', '2'].forEach(function(patchNum) {
+ assert.isFalse(element._computeLeftDisabled(patchNum, patchRange));
+ });
+ assert.isTrue(element._computeLeftDisabled('3', patchRange));
+
+ patchRange.basePatchNum = '2';
+ assert.isTrue(element._computeLeftDisabled('3', patchRange));
+ assert.isTrue(element._computeRightDisabled('1', patchRange));
+ assert.isTrue(element._computeRightDisabled('2', patchRange));
+ assert.isFalse(element._computeRightDisabled('3', patchRange));
+ });
+
+ test('navigation', function(done) {
+ var showStub = sinon.stub(page, 'show');
+ var leftSelectEl = element.$.leftPatchSelect;
+ var rightSelectEl = element.$.rightPatchSelect;
+ element.changeNum = '42';
+ element.path = 'path/to/file.txt';
+ element.availablePatches = ['1', '2', '3'];
+ flushAsynchronousOperations();
+
+ var numEvents = 0;
+ leftSelectEl.addEventListener('change', function(e) {
+ numEvents++;
+ if (numEvents == 1) {
+ assert(showStub.lastCall.calledWithExactly(
+ '/c/42/3/path/to/file.txt'),
+ 'Should navigate to /c/42/3/path/to/file.txt');
+ leftSelectEl.value = '1';
+ element.fire('change', {}, {node: leftSelectEl});
+ } else if (numEvents == 2) {
+ assert(showStub.lastCall.calledWithExactly(
+ '/c/42/1..3/path/to/file.txt'),
+ 'Should navigate to /c/42/1..3/path/to/file.txt');
+ showStub.restore();
+ done();
+ }
+ });
+ leftSelectEl.value = 'PARENT';
+ rightSelectEl.value = '3';
+ element.fire('change', {}, {node: leftSelectEl});
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/test/gr-reply-dropdown-test.html b/polygerrit-ui/app/test/gr-reply-dropdown-test.html
index 2089c8e..4752017 100644
--- a/polygerrit-ui/app/test/gr-reply-dropdown-test.html
+++ b/polygerrit-ui/app/test/gr-reply-dropdown-test.html
@@ -21,6 +21,7 @@
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<script src="../scripts/changes.js"></script>
+<script src="../scripts/fake-app.js"></script>
<script src="../scripts/util.js"></script>
<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
@@ -115,44 +116,50 @@
assert.isFalse(element.opened);
MockInteractions.tap(element.$.trigger);
assert.isTrue(element.opened);
- for (var label in element.permittedLabels) {
- assert.ok(element.$$('iron-selector[data-label="' + label + '"]'),
- label);
- }
- element._draft = 'I wholeheartedly disapprove';
- MockInteractions.tap(element.$$(
- 'iron-selector[data-label="Code-Review"] > button[data-value="-1"]'));
- MockInteractions.tap(element.$$(
- 'iron-selector[data-label="Verified"] > button[data-value="-1"]'));
- // This is needed on non-Blink engines most likely due to the ways in
- // which the dom-repeat elements are stamped.
+ // Without this, the test fails to register taps on the label buttons.
+ // TODO(andybons): Look into test flakiness with Polymer team to figure
+ // out why exactly this is happening.
element.async(function() {
- MockInteractions.tap(element.$$('.send'));
- assert.isTrue(element.disabled);
+ for (var label in element.permittedLabels) {
+ assert.ok(element.$$('iron-selector[data-label="' + label + '"]'),
+ label);
+ }
+ element._draft = 'I wholeheartedly disapprove';
+ MockInteractions.tap(element.$$(
+ 'iron-selector[data-label="Code-Review"] > button[data-value="-1"]'));
+ MockInteractions.tap(element.$$(
+ 'iron-selector[data-label="Verified"] > button[data-value="-1"]'));
- server.respond();
+ // This is needed on non-Blink engines most likely due to the ways in
+ // which the dom-repeat elements are stamped.
+ element.async(function() {
+ MockInteractions.tap(element.$$('.send'));
+ assert.isTrue(element.disabled);
- element._xhrPromise.then(function(req) {
- assert.isFalse(element.disabled,
- 'Element should be enabled when done sending reply.');
- assert.isFalse(element.opened);
- assert.equal(req.status, 200);
- assert.equal(req.url, '/changes/42/revisions/1/review');
- var reqObj = JSON.parse(req.xhr.requestBody);
- assert.deepEqual(reqObj, {
- drafts: 'PUBLISH_ALL_REVISIONS',
- labels: {
- 'Code-Review': -1,
- 'Verified': -1
- },
- message: 'I wholeheartedly disapprove'
+ server.respond();
+
+ element._xhrPromise.then(function(req) {
+ assert.isFalse(element.disabled,
+ 'Element should be enabled when done sending reply.');
+ assert.isFalse(element.opened);
+ assert.equal(req.status, 200);
+ assert.equal(req.url, '/changes/42/revisions/1/review');
+ var reqObj = JSON.parse(req.xhr.requestBody);
+ assert.deepEqual(reqObj, {
+ drafts: 'PUBLISH_ALL_REVISIONS',
+ labels: {
+ 'Code-Review': -1,
+ 'Verified': -1
+ },
+ message: 'I wholeheartedly disapprove'
+ });
+ assert.equal(req.response.labels['Code-Review'], -1);
+ assert.equal(req.response.labels['Verified'], -1);
+ assert.isFalse(element.opened);
+ done();
});
- assert.equal(req.response.labels['Code-Review'], -1);
- assert.equal(req.response.labels['Verified'], -1);
- assert.isFalse(element.opened);
- done();
- });
+ }, 1);
}, 1);
});
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 6049a4f..a57ccc4 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -30,7 +30,12 @@
'gr-date-formatter-test.html',
'gr-diff-comment-test.html',
'gr-diff-comment-thread-test.html',
+ 'gr-diff-side-test.html',
+ 'gr-diff-test.html',
'gr-diff-view-test.html',
+ 'gr-file-list-test.html',
+ 'gr-linked-text-test.html',
+ 'gr-patch-range-select-test.html',
'gr-reply-dropdown-test.html',
'gr-search-bar-test.html',
].forEach(function(file) {
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 5fe6e36..59a09b0 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -50,6 +50,7 @@
http.HandleFunc("/changes/", handleRESTProxy)
http.HandleFunc("/accounts/", handleRESTProxy)
http.HandleFunc("/config/", handleRESTProxy)
+ http.HandleFunc("/projects/", handleRESTProxy)
http.HandleFunc("/accounts/self/detail", handleAccountDetail)
log.Println("Serving on port", *port)
log.Fatal(http.ListenAndServe(*port, &server{}))
diff --git a/tools/maven/BUCK b/tools/maven/BUCK
index 98a7ade..fcd77c0 100644
--- a/tools/maven/BUCK
+++ b/tools/maven/BUCK
@@ -1,12 +1,14 @@
include_defs('//VERSION')
include_defs('//tools/maven/package.defs')
+include_defs('//tools/maven/repository.defs')
-URL = 'https://oss.sonatype.org/content/repositories/snapshots' \
- if GERRIT_VERSION.endswith('-SNAPSHOT') else \
- 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
+if GERRIT_VERSION.endswith('-SNAPSHOT'):
+ URL = MAVEN_SNAPSHOT_URL
+else:
+ URL = MAVEN_RELEASE_URL
maven_package(
- repository = 'sonatype-nexus-staging',
+ repository = MAVEN_REPOSITORY,
url = URL,
version = GERRIT_VERSION,
jar = {
diff --git a/tools/maven/repository.defs b/tools/maven/repository.defs
new file mode 100644
index 0000000..c4e8fbf
--- /dev/null
+++ b/tools/maven/repository.defs
@@ -0,0 +1,3 @@
+MAVEN_REPOSITORY = 'sonatype-nexus-staging'
+MAVEN_SNAPSHOT_URL = 'https://oss.sonatype.org/content/repositories/snapshots'
+MAVEN_RELEASE_URL = 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
diff --git a/website/releases/index.html b/website/releases/index.html
index 456f0f9..582b495 100644
--- a/website/releases/index.html
+++ b/website/releases/index.html
@@ -31,7 +31,7 @@
<h1>Gerrit Code Review - Releases</h1>
<a href="https://www.gerritcodereview.com/">
- <img id="diffy_logo" src="https://gerrit-review.googlesource.com/static/diffy1.cache.png">
+ <img id="diffy_logo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAtCAYAAADoSujCAAAABGdBTUEAALGPC/xhBQAACkFpQ0NQSUNDIFByb2ZpbGUAAEgNnZZ3VFPZFofPvTe90BIiICX0GnoJINI7SBUEUYlJgFAChoQmdkQFRhQRKVZkVMABR4ciY0UUC4OCYtcJ8hBQxsFRREXl3YxrCe+tNfPemv3HWd/Z57fX2Wfvfde6AFD8ggTCdFgBgDShWBTu68FcEhPLxPcCGBABDlgBwOFmZgRH+EQC1Py9PZmZqEjGs/buLoBku9ssv1Amc9b/f5EiN0MkBgAKRdU2PH4mF+UClFOzxRky/wTK9JUpMoYxMhahCaKsIuPEr2z2p+Yru8mYlybkoRpZzhm8NJ6Mu1DemiXho4wEoVyYJeBno3wHZb1USZoA5fco09P4nEwAMBSZX8znJqFsiTJFFBnuifICAAiUxDm8cg6L+TlongB4pmfkigSJSWKmEdeYaeXoyGb68bNT+WIxK5TDTeGIeEzP9LQMjjAXgK9vlkUBJVltmWiR7a0c7e1Z1uZo+b/Z3x5+U/09yHr7VfEm7M+eQYyeWd9s7KwvvRYA9iRamx2zvpVVALRtBkDl4axP7yAA8gUAtN6c8x6GbF6SxOIMJwuL7OxscwGfay4r6Df7n4Jvyr+GOfeZy+77VjumFz+BI0kVM2VF5aanpktEzMwMDpfPZP33EP/jwDlpzcnDLJyfwBfxhehVUeiUCYSJaLuFPIFYkC5kCoR/1eF/GDYnBxl+nWsUaHVfAH2FOVC4SQfIbz0AQyMDJG4/egJ961sQMQrIvrxorZGvc48yev7n+h8LXIpu4UxBIlPm9gyPZHIloiwZo9+EbMECEpAHdKAKNIEuMAIsYA0cgDNwA94gAISASBADlgMuSAJpQASyQT7YAApBMdgBdoNqcADUgXrQBE6CNnAGXARXwA1wCwyAR0AKhsFLMAHegWkIgvAQFaJBqpAWpA+ZQtYQG1oIeUNBUDgUA8VDiZAQkkD50CaoGCqDqqFDUD30I3Qaughdg/qgB9AgNAb9AX2EEZgC02EN2AC2gNmwOxwIR8LL4ER4FZwHF8Db4Uq4Fj4Ot8IX4RvwACyFX8KTCEDICAPRRlgIG/FEQpBYJAERIWuRIqQCqUWakA6kG7mNSJFx5AMGh6FhmBgWxhnjh1mM4WJWYdZiSjDVmGOYVkwX5jZmEDOB+YKlYtWxplgnrD92CTYRm40txFZgj2BbsJexA9hh7DscDsfAGeIccH64GFwybjWuBLcP14y7gOvDDeEm8Xi8Kt4U74IPwXPwYnwhvgp/HH8e348fxr8nkAlaBGuCDyGWICRsJFQQGgjnCP2EEcI0UYGoT3QihhB5xFxiKbGO2EG8SRwmTpMUSYYkF1IkKZm0gVRJaiJdJj0mvSGTyTpkR3IYWUBeT64knyBfJQ+SP1CUKCYUT0ocRULZTjlKuUB5QHlDpVINqG7UWKqYup1aT71EfUp9L0eTM5fzl+PJrZOrkWuV65d7JU+U15d3l18unydfIX9K/qb8uAJRwUDBU4GjsFahRuG0wj2FSUWaopViiGKaYolig+I1xVElvJKBkrcST6lA6bDSJaUhGkLTpXnSuLRNtDraZdowHUc3pPvTk+nF9B/ovfQJZSVlW+Uo5RzlGuWzylIGwjBg+DNSGaWMk4y7jI/zNOa5z+PP2zavaV7/vCmV+SpuKnyVIpVmlQGVj6pMVW/VFNWdqm2qT9QwaiZqYWrZavvVLquNz6fPd57PnV80/+T8h+qwuol6uPpq9cPqPeqTGpoavhoZGlUalzTGNRmabprJmuWa5zTHtGhaC7UEWuVa57VeMJWZ7sxUZiWzizmhra7tpy3RPqTdqz2tY6izWGejTrPOE12SLls3Qbdct1N3Qk9LL1gvX69R76E+UZ+tn6S/R79bf8rA0CDaYItBm8GooYqhv2GeYaPhYyOqkavRKqNaozvGOGO2cYrxPuNbJrCJnUmSSY3JTVPY1N5UYLrPtM8Ma+ZoJjSrNbvHorDcWVmsRtagOcM8yHyjeZv5Kws9i1iLnRbdFl8s7SxTLessH1kpWQVYbbTqsPrD2sSaa11jfceGauNjs86m3ea1rakt33a/7X07ml2w3Ra7TrvP9g72Ivsm+zEHPYd4h70O99h0dii7hH3VEevo4bjO8YzjByd7J7HTSaffnVnOKc4NzqMLDBfwF9QtGHLRceG4HHKRLmQujF94cKHUVduV41rr+sxN143ndsRtxN3YPdn9uPsrD0sPkUeLx5Snk+cazwteiJevV5FXr7eS92Lvau+nPjo+iT6NPhO+dr6rfS/4Yf0C/Xb63fPX8Of61/tPBDgErAnoCqQERgRWBz4LMgkSBXUEw8EBwbuCHy/SXyRc1BYCQvxDdoU8CTUMXRX6cxguLDSsJux5uFV4fnh3BC1iRURDxLtIj8jSyEeLjRZLFndGyUfFRdVHTUV7RZdFS5dYLFmz5EaMWowgpj0WHxsVeyR2cqn30t1Lh+Ps4grj7i4zXJaz7NpyteWpy8+ukF/BWXEqHhsfHd8Q/4kTwqnlTK70X7l35QTXk7uH+5LnxivnjfFd+GX8kQSXhLKE0USXxF2JY0muSRVJ4wJPQbXgdbJf8oHkqZSQlKMpM6nRqc1phLT4tNNCJWGKsCtdMz0nvS/DNKMwQ7rKadXuVROiQNGRTChzWWa7mI7+TPVIjCSbJYNZC7Nqst5nR2WfylHMEeb05JrkbssdyfPJ+341ZjV3dWe+dv6G/ME17msOrYXWrlzbuU53XcG64fW+649tIG1I2fDLRsuNZRvfbore1FGgUbC+YGiz7+bGQrlCUeG9Lc5bDmzFbBVs7d1ms61q25ciXtH1YsviiuJPJdyS699ZfVf53cz2hO29pfal+3fgdgh33N3puvNYmWJZXtnQruBdreXM8qLyt7tX7L5WYVtxYA9pj2SPtDKosr1Kr2pH1afqpOqBGo+a5r3qe7ftndrH29e/321/0wGNA8UHPh4UHLx/yPdQa61BbcVh3OGsw8/rouq6v2d/X39E7Ujxkc9HhUelx8KPddU71Nc3qDeUNsKNksax43HHb/3g9UN7E6vpUDOjufgEOCE58eLH+B/vngw82XmKfarpJ/2f9rbQWopaodbc1om2pDZpe0x73+mA050dzh0tP5v/fPSM9pmas8pnS8+RzhWcmzmfd37yQsaF8YuJF4c6V3Q+urTk0p2usK7ey4GXr17xuXKp2737/FWXq2euOV07fZ19ve2G/Y3WHruell/sfmnpte9tvelws/2W462OvgV95/pd+y/e9rp95Y7/nRsDiwb67i6+e/9e3D3pfd790QepD14/zHo4/Wj9Y+zjoicKTyqeqj+t/dX412apvfTsoNdgz7OIZ4+GuEMv/5X5r0/DBc+pzytGtEbqR61Hz4z5jN16sfTF8MuMl9Pjhb8p/rb3ldGrn353+71nYsnE8GvR65k/St6ovjn61vZt52To5NN3ae+mp4req74/9oH9oftj9MeR6exP+E+Vn40/d3wJ/PJ4Jm1m5t/3hPP7MjpZfgAAAAlwSFlzAAAN1wAADdcBQiibeAAADVtJREFUaAXNWQl0FEUa/qu7ekKGJJBkQg4SQsKhCZHlCCJKkKCIoBtWJCggCusK+9xdUFFUEJldn3sIeLAYH1lkOfbBI0BwMUIEISEi4TCGmxBIyA05h4RJJjPT3bV/dc9MghKut0r+96qrp7qO7/uvqu4B+P8IYYwR11RBx8+dW52bl/epe+rTp08b3PedsXYDh51ZWZMriovzWX4+a75czfbknXjVDTgtjSEJRrFIQ4d+L2VlMYqkJV6ysrKw/S4J17xb+zt3716A94z9kMdY5pdMsTWysppzf80yw00B4ijBbDYLd4UGLuwBeLasZL61wSJbdmazspOX2IHDtexPLx/ZLIqls/veU/6iKO6cnpT01vRdu1a/UFqaM/nw4QMJCLqbG/jtkvCY3z3BHdQcvOwa5x/v/9zvRv8+abEdfH33Z1Q7rE1+Bm/fYCg46wOUBoMs+yn9+7fAY49ZhIQEh/3++43NPj7GqvPnK5cNHz5kPVqCzJkzh6ampvI52R3guZ0hsyV3b1EUn9+zN6uwvKKKvf7GSkUUk1VRXMBEcaEsiksUX98PGKVrmMGQh21MKzExjL3//iV26lRBY3V1WeXRowWT3fPxmsdGcnKa2L7tx/d3agEyerRZzM42cy11W/b3T5aMHjdyrtXbKp6/fA7uC74PrBUqe+ftXeRofgN26c569waFsRZoaWnB3wlgt/+GNTX5oIZVDO7yfZmZF2v9/WV7fPzYJdihDIuKRZO0tDRxypQp/PdPLHInBHBMGgbbFGXw4OmxJ044Uj5LefbhiZMS4Gh5rqOwplB4atgksXdANCkuLoOt23Jg4dvHcW0nFj8NEALG+nEskxCUhHM5bIxlFC1bKpseHRtjM3o7S47lV5Y+M/WbfYqyYjt2tHLXIuTPuLbZQ4xPdrsEPOAl6fFhAHEbVNV0z4BYGcKjBOfQeJP08Mg46NMnHCIjegIhelLhRHJzT8H+/efgwAErWK3N0KuXEYYOfRrCwwPU+vpKIXOrCcpKQuHNf1CYmCRDvz4EKsurYVv62dwFC1a8KctffcsBIwGctI3EbRLg2cYsS9K4Iap632ZCgvp26eKQbbZWnNiBzwzQkxrgwSQfiPuVCYYMiYLY2CgIDQ0Cg4GCzWaHZmsLKCqDVrsNqi83wtGjBkhPD1L3HwwgmpHAhm5pEd5bKojJk7tD34gu8G3OD/VjHlkxTZbX7dY46IrX3Ok2CPCATUU/6B9N6RNo1tCBAK1OxhQxNFQUunYV4MIF7iZ2LDw0dJf19fWGZ54NhcGDekJUVA/o1s0HidjgwvkSyNrXFzZvidcwJQeWsafq9xLDjJNg63uFeS0JZfkwWumzcSBMnRgk1dQUVu7NOTR+1nOzTqI7iehPCh94iwR0zWN/b0rnbgIIm4gaR5QKmpMIfC9DwMTXh0BgoAiCSKCpSYW6Or6Gm5SDr4fijcWCpSeWP2AxwTtRe+DVi+NBGYTUcXYhFOAiHkSMiwBWwx711Ix4+dO3JEPT1QPrhw0b+wIO0jJUYmKirDspb+lYuM9xlWIef+kVgEAEz0FxDetOTggjgkDgqhWgpFSG4mIn+rmKriNCZKQ39IroDv6BQdg/EIsRC5LwGYBGNAF0BSg1RkItPAHCKHTCkCjYn9MPRiyKAflJgFd91wpZG1QhdkAXWPZez/EAiyJxAtxlfLnyyQ1zLO8IsAp9O0OVpAnxwCJXAOoZCXB1evYArRteCE7JiaB5wYkcOYnGRixNDLwog2iTAHWqBK8NaIC1D+yAEQYjpF+IhhNqCBTZRkFAjRcYB2ZAUV0D2L6og+kVAAGOk2AZ+DQJHO8kYcYqY8IjxedyD+bk4UanMqYt6V7+enUyEtyi+ZokzV3NWPiL6PdoDYbtntPn9QZqbZwQjzRcCPoggSInhX70Cmz/9W64N/iMZsidpR9D0jezEakBwhqa4NHQfBg8+luILzgDfidNsEmeCiEfV8DMWVNBwlVtrb3tTU0lR/btExbMmuU8dJMYWIVanuOk9LkHAcIzALz8cVWMUi8vHVqH2D0POIEoA4OLdjQk2CB7Qg6MDM8FVe6NsVICDD1x7dlN8NLBZP1E1AgQDA6IIc2QHYTGrpEgK/sTljByPvdZXhCTCJWVSvHKlco45NShoO9naNoXhPgpjDmGADRfJaRrAEYDH8Sx3VABvEM4ar6MZ1gElZ6YB2Mj9+NIA1qlHlS1LxPEBoj228aYPEY9WNGLZ2JoRgOX+HkzaBARsFV9Z9EG8Pc/hlgHYbnMMSl+fsQUEiIilY6FBzjHIIiiXxxAj3hCwqJ1zHacWMDn18fPW/nAMARfKfMlFFiXcBye7peJIwSFqYIiYCUQPGawSOhiaBSGBeUI/X16Qo3Fn1W0YmTjxovDcA1JmDq1TPD1zQRFkTEWW3BCQcNttzNMEx2KGZ9layYThEkL0IsTGLMUowYshHihFdCLQECcP40F7vMBmFlrFGSAZv90+Bl15r3/VXFZAQSDQAQH3qEC+G/xCvINRhIVMChkC0mOOkQeDRVIsNiVlDX7QZPsgBPHjWcuXJCW+PvvT7XZglsIaQ43GqFrSwvpdn0VaqTMaEwzZpswE6XT9iIO9L2yLxnr/Swh3XuhVpEc41a6Rjji8B4qyA5RvWwR4G+Di4Q34jdofZzV3kC62UA2EgW3QItDFqrtdvWSXYYyuxrWahCViACvahMqQD5eDzUrTnxY9NWl2COKYsnBs1ete6HFi2HA889L86xWZroRAXRcnv+nBlPaPJsQ3zBV7fkY1tGYidAPnWihLjgnNyKHzYUoEm5rDnQQbvp5/avg3eGfg1G1t9SGdWXO/i3GioNs1eF1Quq/nc66UwD1OIhv3Vqs8RkAVqBpQ3DCKe6dT2vlKRMgWSBEz4paI1/EdXOdyszdB2VTNSF+NYyFzcQawQciKV9EzTcz93Dc1hlxSAYiOhQ89HDfYnVVI2JyMq8aW5d9tqn3qAMjbXPDZwHp/QEJ88pyOv5Tp2UUfrbWwLMsPTMAzEVCbvDcPbWUjcCxJ2xRzWY+tyd2nW4E/OmPxOx+hu+8gRMwiFHdcbhgJALka2KKw4DECzIRRF8/L4MDl46Jqb+yddsPsDVt4+bk9OPjI1LUN+Y5i/ICfZjJUgstJm9IemEgPSVW0YLsfVLq9u3SUL4wSQQ5C9+dzZw8guQ16IKKJFyZPKwZElB5eHOLcDIIpiPJdj1YRdFsRaoaUUWIN6I+i+140AE/NLOClvDC4sBDWn15YyP7MDb2y8IJ44vSHQ427uXX+kSkfFhUuWK5NFNtZV3yj0GSjzfzMwULo69Y2CibXR1DKem3caO0fto054ZEM8gcFEGQ+uIdezgng31+mkFcqHnFR2vOLUnScMYWvIuug5a4im0iFooa4o+tBUajdWtT05G1ZnNe+bBh4vTBg32WmUzOgLo6e83lKlJy4hQ5tGOHc2l6OlTwibm88gqEjhgujcfT9QOtrWDEI0fawoXKDv1p29qu3x1WHVPUhuiZCAkMYWzGLoCgHhhz6CjdMdAcBQDVn1O6b0tr68VS3n3evF5PjhnT+GBoaMtFb29Q/P1hlMWiJlZVgWS1kkLskiVJ8takJDitTY+XmTMhJD8f/B56CMSUFLiITfzlwqM8d787qNs+NknSmn9SmoIv5ctbKc3B+pBTFP+Fp1K3JIvh4cneycnaWdndqNVxcRD89dfS7G3bpLQ1a2jmunV0KYLuxx+iu1zjwjge3fGaNkzd2qGRt7vLTZSOPXVhfDAeoTMTKL10hdIGBF7bTPF7GqWW71B33V39rgGhpzv9SfsrzxwIPOSjjyC6XRbRuriAt+/OA9gdxO3bb/VeB4+9fSkt+kYHzexYq/r9scW3OtPP3O961mhzHUrzP3CBR63L6D78e07dZUnaFP8zA7vV6blbtRd+/tdFkvbOpNSKwLnLqHikZrwwSbqw1t2nE9QevIiFb1z62YbStY9QWmfRwXPgGgEkcsVKacY4HbjHze4mD0+coC8xVzC+HieKVUUu8A5KFW4Fl/aLNrahvfEnv7Z+P+udRqAdeIigtOxom+abEbyCJLTMgxbZnKjD6RTa51A4ge+1dIk3PSi9kO0Cj9mmCrVeysFrmUeSClL4CF24u3UK8cSAP6WFmS7wqPU69PUjtZTaNe2LYnUJwF/wOwiXTqN9DkZTZDdKC75oA29rwd32MJKo09tUJPT927w3gud5l5fOIgR32uPtwHOwhzMoPZvbRqjyIKLFrxFc+FcKj9xtItr6eFQWWnFrd0lhJiGNFxkb8oTe0ORg7ORyvMdvgdx1CH+LcQv3P+2LnbvhF67d6y+7F90Ffb22QJKW/1YUG8rd2pekc+vbQLVtcq42TwC19flF79rWl6Q9f5Sk1BmSVJTmBo9HhlKA+bE6JE+m+kUR3tZilH73phs8nnswFg7N1yfQdue77e8dcXH9haI99nIdj/mPyr2y/MBKfdQWDt4TJXqbdu0sewF/69ckkNLKY5Q2ofbXP6w33dB1OgMBrty2XVUUtyVRuvN9HTy/3vSdua3rXbr7H0SXfo3+OPT1AAAAAElFTkSuQmCC" />
</a>
<div id='download_container'>