Merge branch 'stable-3.1'
* stable-3.1:
ChildProjectApiImpl: Fix error message
PutDescription: Use present tense for commit message
Use the same font everywhere in gr-diff
Remove css rules for font-size-normal where not required
Add css classes for fonts to shared styles
Move text color default from :host rule to main.css
Use font-family var in main.css
Move font-family css var
Remove theme comment about font-weight-bold
Add css vars for line-height
Introduce -h1 -h2 -h3 css vars for font-size
Consolidate font-size css
Remove redundant font:inherit styles
Remove usage of Open Sans font family
Replace all css spacing by variables in theme files
Change-Id: I437afcb7247bd86455dbb3d3fda00211fd5facf1
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 8dafe7e..bb4deb3 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -3748,6 +3748,19 @@
+
Default is true.
+[[receive.enableInMemoryRefCache]]receive.enableInMemoryRefCache::
++
+If true, Gerrit will cache all refs advertised during push in memory and
+base later receive operations on that cache.
++
+Turning this cache off is considered experimental.
++
+This cache provides value when the ref database is slow and/or does not
+offer an inverse lookup of object ID to ref name. When RefTable is used,
+this cache can be turned off (experimental) to get speed improvements.
++
+Default is true.
+
[[receive.enableSignedPush]]receive.enableSignedPush::
+
If true, server-side signed push validation is enabled.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 6b8281a..b6afad5 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -1501,6 +1501,90 @@
change is new
----
+[[revert-submission]]
+=== Revert Submission
+--
+'POST /changes/link:#change-id[\{change-id\}]/revert_submission'
+--
+
+Creates open revert changes for all of the changes of a certain submission.
+
+Details for the revert can be specified in the request body inside a link:#revert-input[
+RevertInput] The topic of all created revert changes will be
+`revert-{submission_id}-{random_string_of_size_10}`.
+
+The changes will not be rebased on onto the destination branch so the users may still
+have to manually rebase them to resolve conflicts and make them submittable.
+
+.Request
+----
+ POST /changes/myProject~master~I1ffe09a505e25f15ce1521bcfb222e51e62c2a14/revert_submission HTTP/1.0
+----
+
+As response link:#revert-submission-info[RevertSubmissionInfo] entity
+is returned. That entity describes the revert changes.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ "revert_changes":
+ [
+ {
+ "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+ "project": "myProject",
+ "branch": "master",
+ "topic": "revert--1571043962462-3640749-ABCEEZGHIJ",
+ "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+ "subject": "Revert \"Implementing Feature X\"",
+ "status": "NEW",
+ "created": "2013-02-01 09:59:32.126000000",
+ "updated": "2013-02-21 11:16:36.775000000",
+ "mergeable": true,
+ "insertions": 6,
+ "deletions": 4,
+ "_number": 3965,
+ "owner": {
+ "name": "John Doe"
+ }
+ },
+ {
+ "id": "anyProject~master~1eee2c9d8f352483781e772f35dc586a69ff5646",
+ "project": "anyProject",
+ "branch": "master",
+ "topic": "revert--1571043962462-3640749-ABCEEZGHIJ",
+ "change_id": "I1eee2c9d8f352483781e772f35dc586a69ff5646",
+ "subject": "Revert \"Implementing Feature Y\"",
+ "status": "NEW",
+ "created": "2013-02-04 09:59:33.126000000",
+ "updated": "2013-02-21 11:16:37.775000000",
+ "mergeable": true,
+ "insertions": 62,
+ "deletions": 11,
+ "_number": 3966,
+ "owner": {
+ "name": "Jane Doe"
+ }
+ }
+ ]
+----
+
+If any of the changes cannot be reverted because the change state doesn't
+allow reverting the change, the response is "`409 Conflict`" and
+the error message is contained in the response body.
+
+.Response
+----
+ HTTP/1.1 409 Conflict
+ Content-Disposition: attachment
+ Content-Type: text/plain; charset=UTF-8
+
+ change is new
+----
+
[[submit-change]]
=== Submit Change
--
@@ -6931,10 +7015,24 @@
Additional information about whom to notify about the revert as a map
of recipient type to link:#notify-info[NotifyInfo] entity.
|`topic` |optional|
-Name of the topic for the revert change. If not set, the default is the topic
-of the change being reverted.
+Name of the topic for the revert change. If not set, the default for Revert
+endpoint is the topic of the change being reverted, and the default for the
+RevertSubmission endpoint is `revert-{submission_id}-{timestamp.now}`.
|=============================
+[[revert-submission-info]]
+=== RevertSubmissionInfo
+The `RevertSubmissionInfo` describes the revert changes.
+
+[options="header",cols="1,6"]
+|==============================
+|Field Name | Description
+|`revert_changes` |
+A list of link:#change-info[ChangeInfo] that describes the revert changes. Each
+entity in that list is a revert change that was created in that revert
+submission.
+|==============================
+
[[review-info]]
=== ReviewInfo
The `ReviewInfo` entity contains information about a review.
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index b70dfea..c26d271 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -2976,6 +2976,314 @@
HTTP/1.1 204 No Content
----
+[[label-endpoints]]
+== Label Endpoints
+
+[[list-labels]]
+=== List Labels
+--
+'GET /projects/link:#project-name[\{project-name\}]/labels/'
+--
+
+Lists the labels that are defined in this project.
+
+The calling user must have read access to the `refs/meta/config` branch of the
+project.
+
+.Request
+----
+ GET /projects/All-Projects/labels/ HTTP/1.0
+----
+
+As result a list of link:#label-definition-info[LabelDefinitionInfo] entities
+is returned that describe the labels that are defined in this project
+(inherited labels are not returned unless the `inherited` parameter is set, see
+link:#list-with-inherited-labels[below]). The returned labels are sorted by
+label name.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ [
+ {
+ "name": "Code-Review",
+ "project": "All-Projects",
+ "function": "MaxWithBlock",
+ "values": {
+ " 0": "No score",
+ "-1": "I would prefer this is not merged as is",
+ "-2": "This shall not be merged",
+ "+1": "Looks good to me, but someone else must approve",
+ "+2": "Looks good to me, approved"
+ },
+ "default_value": 0,
+ "can_override": true,
+ "copy_min_score": true,
+ "copy_all_scores_if_no_change": true,
+ "copy_all_scores_on_trivial_rebase": true,
+ "allow_post_submit": true
+ }
+ ]
+----
+
+[[list-with-inherited-labels]]
+To include inherited labels from all parent projects the parameter `inherited`
+can be set.
+
+The calling user must have read access to the `refs/meta/config` branch of the
+project and all its parent projects.
+
+.Request
+----
+ GET /projects/My-Project/labels/?inherited HTTP/1.0
+----
+
+As result a list of link:#label-definition-info[LabelDefinitionInfo] entities
+is returned that describe the labels that are defined in this project and in
+all its parent projects. The returned labels are sorted by parent projects
+in-order from `All-Projects` through the project hierarchy to this project.
+Labels that belong to the same project are sorted by label name.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ [
+ {
+ "name": "Code-Review",
+ "project": "All-Projects",
+ "function": "MaxWithBlock",
+ "values": {
+ " 0": "No score",
+ "-1": "I would prefer this is not merged as is",
+ "-2": "This shall not be merged",
+ "+1": "Looks good to me, but someone else must approve",
+ "+2": "Looks good to me, approved"
+ },
+ "default_value": 0,
+ "can_override": true,
+ "copy_min_score": true,
+ "copy_all_scores_if_no_change": true,
+ "copy_all_scores_on_trivial_rebase": true,
+ "allow_post_submit": true
+ },
+ {
+ "name": "Foo-Review",
+ "project": "My-Project",
+ "function": "MaxWithBlock",
+ "values": {
+ " 0": "No score",
+ "-1": "I would prefer this is not merged as is",
+ "-2": "This shall not be merged",
+ "+1": "Looks good to me, but someone else must approve",
+ "+2": "Looks good to me, approved"
+ },
+ "default_value": 0,
+ "can_override": true,
+ "copy_any_score": true,
+ "allow_post_submit": true
+ }
+ ]
+----
+
+[[get-label]]
+=== Get Label
+--
+'GET /projects/link:#project-name[\{project-name\}]/labels/link:#label-name[\{label-name\}]'
+--
+
+Retrieves the definition of a label that is defined in this project.
+
+The calling user must have read access to the `refs/meta/config` branch of the
+project.
+
+.Request
+----
+ GET /projects/All-Projects/labels/Code-Review HTTP/1.0
+----
+
+As response a link:#label-definition-info[LabelDefinitionInfo] entity is
+returned that describes the label.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "name": "Code-Review",
+ "project": "All-Projects",
+ "function": "MaxWithBlock",
+ "values": {
+ " 0": "No score",
+ "-1": "I would prefer this is not merged as is",
+ "-2": "This shall not be merged",
+ "+1": "Looks good to me, but someone else must approve",
+ "+2": "Looks good to me, approved"
+ },
+ "default_value": 0,
+ "can_override": true,
+ "copy_min_score": true,
+ "copy_all_scores_if_no_change": true,
+ "copy_all_scores_on_trivial_rebase": true,
+ "allow_post_submit": true
+ }
+----
+
+[[create-label]]
+=== Create Label
+--
+'PUT /projects/link:#project-name[\{project-name\}]/labels/link:#label-name[\{label-name\}]'
+--
+
+Creates a new label definition in this project.
+
+The calling user must have write access to the `refs/meta/config` branch of the
+project.
+
+If a label with this name is already defined in this project, this label
+definition is updated (see link:#set-label[Set Label]).
+
+.Request
+----
+ PUT /projects/My-Project/labels/Foo HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "commit_message": "Create Foo Label",
+ "values": {
+ " 0": "No score",
+ "-1": "I would prefer this is not merged as is",
+ "-2": "This shall not be merged",
+ "+1": "Looks good to me, but someone else must approve",
+ "+2": "Looks good to me, approved"
+ }
+ }
+----
+
+As response a link:#label-definition-info[LabelDefinitionInfo] entity is
+returned that describes the created label.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "name": "Foo",
+ "project_name": "My-Project",
+ "function": "MaxWithBlock",
+ "values": {
+ " 0": "No score",
+ "-1": "I would prefer this is not merged as is",
+ "-2": "This shall not be merged",
+ "+1": "Looks good to me, but someone else must approve",
+ "+2": "Looks good to me, approved"
+ },
+ "default_value": 0,
+ "can_override": true,
+ "copy_all_scores_if_no_change": true,
+ "allow_post_submit": true
+ }
+----
+
+[[set-label]]
+=== Set Label
+--
+'PUT /projects/link:#project-name[\{project-name\}]/labels/link:#label-name[\{label-name\}]'
+--
+
+Updates the definition of a label that is defined in this project.
+
+The calling user must have write access to the `refs/meta/config` branch of the
+project.
+
+Properties which are not set in the input entity are not modified.
+
+.Request
+----
+ PUT /projects/All-Projects/labels/Code-Review HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "commit_message": "Ignore self approvals for Code-Review label",
+ "ignore_self_approval": true
+ }
+----
+
+As response a link:#label-definition-info[LabelDefinitionInfo] entity is
+returned that describes the updated label.
+
+.Response
+----
+ HTTP/1.1 200 OK
+ Content-Disposition: attachment
+ Content-Type: application/json; charset=UTF-8
+
+ )]}'
+ {
+ "name": "Code-Review",
+ "project": "All-Projects",
+ "function": "MaxWithBlock",
+ "values": {
+ " 0": "No score",
+ "-1": "I would prefer this is not merged as is",
+ "-2": "This shall not be merged",
+ "+1": "Looks good to me, but someone else must approve",
+ "+2": "Looks good to me, approved"
+ },
+ "default_value": 0,
+ "can_override": true,
+ "copy_min_score": true,
+ "copy_all_scores_if_no_change": true,
+ "copy_all_scores_on_trivial_rebase": true,
+ "allow_post_submit": true,
+ "ignore_self_approval": true
+ }
+----
+
+[[delete-label]]
+=== Delete Label
+--
+'DELETE /projects/link:#project-name[\{project-name\}]/labels/link:#label-name[\{label-name\}]'
+--
+
+Deletes the definition of a label that is defined in this project.
+
+The calling user must have write access to the `refs/meta/config` branch of the
+project.
+
+The request body does not need to include a link:#delete-label-input[
+DeleteLabelInput] entity if no commit message is specified.
+
+.Request
+----
+ Delete /projects/My-Project/labels/Foo-Review HTTP/1.0
+ Content-Type: application/json; charset=UTF-8
+
+ {
+ "commit_message": "Delete Foo-Review label",
+ }
+----
+
+If a label was deleted the response is "`204 No Content`".
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
[[ids]]
== IDs
@@ -3000,6 +3308,10 @@
A special dashboard ID is `default` which represents the default
dashboard of a project.
+[[label-name]]
+=== \{label-name\}
+The name of a review label.
+
[[project-name]]
=== \{project-name\}
The name of the project.
@@ -3173,6 +3485,25 @@
|`enabled` |optional|Whether the commentlink is enabled, as documented
in link:config-gerrit.html#commentlink.name.enabled[
commentlink.name.enabled]. If not set the commentlink is enabled.
+
+[[commentlink-input]]
+=== CommentLinkInput
+The `CommentLinkInput` entity describes the input for a
+link:config-gerrit.html#commentlink[commentlink].
+
+|==================================================
+[options="header",cols="1,^2,4"]
+|==================================================
+|Field Name | |Description
+|`match` | |A JavaScript regular expression to match
+positions to be replaced with a hyperlink, as documented in
+link:config-gerrit.html#commentlink.name.match[commentlink.name.match].
+|`link` | |The URL to direct the user to whenever the
+regular expression is matched, as documented in
+link:config-gerrit.html#commentlink.name.link[commentlink.name.link].
+|`enabled` |optional|Whether the commentlink is enabled, as documented
+in link:config-gerrit.html#commentlink.name.enabled[
+commentlink.name.enabled]. If not set the commentlink is enabled.
|==================================================
[[config-info]]
@@ -3323,6 +3654,11 @@
Whether empty commits should be rejected when a change is merged.
Can be `TRUE`, `FALSE` or `INHERIT`. +
If not set, this setting is not updated.
+|commentlinks |optional|
+Map of commentlink names to link:#commentlink-input[CommentLinkInput]
+entities to add or update on the project. If the given commentlink
+already exists, it will be updated with the given values, otherwise
+it will be created. If the value is null, that entry is deleted.
|======================================================
[[config-parameter-info]]
@@ -3428,6 +3764,19 @@
Tokens such as `${project}` are not resolved.
|===========================
+[[delete-label-input]]
+=== DeleteLabelInput
+The `DeleteLabelInput` entity contains information for deleting a label
+definition in a project.
+
+[options="header",cols="1,^2,4"]
+|=============================
+|Field Name ||Description
+|`commit_message`|optional|
+Message that should be used to commit the deletion of the label in the
+`project.config` file to the `refs/meta/config` branch.
+|=============================
+
[[delete-branches-input]]
=== DeleteBranchesInput
The `DeleteBranchesInput` entity contains information about branches that should
@@ -3510,6 +3859,123 @@
Not set if there is no parent.
|================================
+[[label-definition-info]]
+=== LabelDefinitionInfo
+The `LabelTypeInfo` entity describes a link:config-labels.html[
+review label].
+
+[options="header",cols="1,^2,4"]
+|=============================
+|Field Name ||Description
+|`name` ||
+The link:config-labels.html#label_name[name] of the label.
+|`project_name` ||
+The name of the project in which this label is defined.
+|`function` ||
+The link:config-labels.html#label_function[function] of the label (can be
+`MaxWithBlock`, `AnyWithBlock`, `MaxNoBlock`, `NoBlock`, `NoOp` and `PatchSetLock`.
+|`values` ||
+The link:config-labels.html#label_value[values] of the label as a map of label
+value to value description. The label values are formatted strings, e.g. "+1"
+instead of "1", " 0" instead of "0".
+|`default_value` ||
+The link:config-labels.html#label_defaultValue[default value] of the label (as
+integer).
+|`branches` |optional|
+A list of link:config-labels.html#label_branch[branches] for which the label
+applies. A branch can be a ref, a ref pattern or a regular expression. If not
+set, the label applies for all branches.
+|`can_override` |`false` if not set|
+Whether this label can be link:config-labels.html#label_canOverride[overridden]
+by child projects.
+|`copy_any_score`|`false` if not set|
+Whether link:config-labels.html#label_copyAnyScore[copyAnyScore] is set on the
+label.
+|`copy_min_score`|`false` if not set|
+Whether link:config-labels.html#label_copyMinScore[copyMinScore] is set on the
+label.
+|`copy_max_score`|`false` if not set|
+Whether link:config-labels.html#label_copyMaxScore[copyMaxScore] is set on the
+label.
+|`copy_all_scores_if_no_change`|`false` if not set|
+Whether link:config-labels.html#label_copyAllScoresIfNoChange[
+copyAllScoresIfNoChange] is set on the label.
+|`copy_all_scores_if_no_code_change`|`false` if not set|
+Whether link:config-labels.html#label_copyAllScoresIfNoCodeChange[
+copyAllScoresIfNoCodeChange] is set on the label.
+|`copy_all_scores_on_trivial_rebase`|`false` if not set|
+Whether link:config-labels.html#label_copyAllScoresOnTrivialRebase[
+copyAllScoresOnTrivialRebase] is set on the label.
+|`copy_all_scores_on_merge_first_parent_update`|`false` if not set|
+Whether link:config-labels.html#label_copyAllScoresOnMergeFirstParentUpdate[
+copyAllScoresOnMergeFirstParentUpdate] is set on the label.
+|`allow_post_submit`|`false` if not set|
+Whether link:config-labels.html#label_allowPostSubmit[allowPostSubmit] is set
+on the label.
+|`ignore_self_approval`|`false` if not set|
+Whether link:config-labels.html#label_ignoreSelfApproval[ignoreSelfApproval] is
+set on the label.
+|=============================
+
+[[label-definition-input]]
+=== LabelDefinitionInput
+The `LabelTypeInput` entity describes a link:config-labels.html[
+review label].
+
+[options="header",cols="1,^2,4"]
+|=============================
+|Field Name ||Description
+|`commit_message`|optional|
+Message that should be used to commit the change of the label in the
+`project.config` file to the `refs/meta/config` branch.
+|`name` |optional|
+The new link:config-labels.html#label_name[name] of the label.
+|`function` |optional|
+The new link:config-labels.html#label_function[function] of the label (can be
+`MaxWithBlock`, `AnyWithBlock`, `MaxNoBlock`, `NoBlock`, `NoOp` and `PatchSetLock`.
+|`values` |optional|
+The new link:config-labels.html#label_value[values] of the label as a map of
+label value to value description. The label values are formatted strings, e.g.
+"+1" instead of "1", " 0" instead of "0".
+|`default_value` |optional|
+The new link:config-labels.html#label_defaultValue[default value] of the label
+(as integer).
+|`branches` |optional|
+The new branches for which the label applies as a list of
+link:config-labels.html#label_branch[branches]. A branch can be a ref, a ref
+pattern or a regular expression. If not set, the label applies for all
+branches.
+|`can_override` |optional|
+Whether this label can be link:config-labels.html#label_canOverride[overridden]
+by child projects.
+|`copy_any_score`|optional|
+Whether link:config-labels.html#label_copyAnyScore[copyAnyScore] is set on the
+label.
+|`copy_min_score`|optional|
+Whether link:config-labels.html#label_copyMinScore[copyMinScore] is set on the
+label.
+|`copy_max_score`|optional|
+Whether link:config-labels.html#label_copyMaxScore[copyMaxScore] is set on the
+label.
+|`copy_all_scores_if_no_change`|optional|
+Whether link:config-labels.html#label_copyAllScoresIfNoChange[
+copyAllScoresIfNoChange] is set on the label.
+|`copy_all_scores_if_no_code_change`|optional|
+Whether link:config-labels.html#label_copyAllScoresIfNoCodeChange[
+copyAllScoresIfNoCodeChange] is set on the label.
+|`copy_all_scores_on_trivial_rebase`|optional|
+Whether link:config-labels.html#label_copyAllScoresOnTrivialRebase[
+copyAllScoresOnTrivialRebase] is set on the label.
+|`copy_all_scores_on_merge_first_parent_update`|optional|
+Whether link:config-labels.html#label_copyAllScoresOnMergeFirstParentUpdate[
+copyAllScoresOnMergeFirstParentUpdate] is set on the label.
+|`allow_post_submit`|optional|
+Whether link:config-labels.html#label_allowPostSubmit[allowPostSubmit] is set
+on the label.
+|`ignore_self_approval`|optional|
+Whether link:config-labels.html#label_ignoreSelfApproval[ignoreSelfApproval] is
+set on the label.
+|=============================
[[label-type-info]]
=== LabelTypeInfo
diff --git a/java/com/google/gerrit/common/data/LabelType.java b/java/com/google/gerrit/common/data/LabelType.java
index f9cd562..14b8310 100644
--- a/java/com/google/gerrit/common/data/LabelType.java
+++ b/java/com/google/gerrit/common/data/LabelType.java
@@ -157,6 +157,10 @@
return name;
}
+ public void setName(String name) {
+ this.name = checkName(name);
+ }
+
public boolean matches(PatchSetApproval psa) {
return psa.labelId().get().equalsIgnoreCase(name);
}
@@ -173,6 +177,7 @@
return canOverride;
}
+ @Nullable
public List<String> getRefPatterns() {
return refPatterns;
}
@@ -198,7 +203,7 @@
}
public void setRefPatterns(List<String> refPatterns) {
- if (refPatterns != null) {
+ if (refPatterns != null && !refPatterns.isEmpty()) {
this.refPatterns =
refPatterns.stream().collect(collectingAndThen(toList(), Collections::unmodifiableList));
} else {
@@ -210,6 +215,10 @@
return values;
}
+ public void setValues(List<LabelValue> values) {
+ this.values = sortValues(values);
+ }
+
public LabelValue getMin() {
if (values.isEmpty()) {
return null;
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 26a1a27..119d941 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -27,6 +27,7 @@
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.MergePatchSetInput;
import com.google.gerrit.extensions.common.PureRevertInfo;
+import com.google.gerrit.extensions.common.RevertSubmissionInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
@@ -161,6 +162,12 @@
*/
ChangeApi revert(RevertInput in) throws RestApiException;
+ default RevertSubmissionInfo revertSubmission() throws RestApiException {
+ return revertSubmission(new RevertInput());
+ }
+
+ RevertSubmissionInfo revertSubmission(RevertInput in) throws RestApiException;
+
/** Create a merge patch set for the change. */
ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException;
@@ -502,6 +509,11 @@
}
@Override
+ public RevertSubmissionInfo revertSubmission(RevertInput in) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
public void rebase(RebaseInput in) throws RestApiException {
throw new NotImplementedException();
}
diff --git a/java/com/google/gerrit/extensions/api/projects/CommentLinkInput.java b/java/com/google/gerrit/extensions/api/projects/CommentLinkInput.java
new file mode 100644
index 0000000..3aad7e1
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/projects/CommentLinkInput.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.projects;
+
+/*
+ * Input for a commentlink configuration on a project.
+ */
+public class CommentLinkInput {
+ /** A JavaScript regular expression to match positions to be replaced with a hyperlink. */
+ public String match;
+ /** The URL to direct the user to whenever the regular expression is matched. */
+ public String link;
+ /** Whether the commentlink is enabled. */
+ public Boolean enabled;
+}
diff --git a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
index 1a6d77b..8005fc5 100644
--- a/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
+++ b/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
@@ -38,4 +38,5 @@
public SubmitType submitType;
public ProjectState state;
public Map<String, Map<String, ConfigValue>> pluginConfigValues;
+ public Map<String, CommentLinkInput> commentLinks;
}
diff --git a/java/com/google/gerrit/extensions/api/projects/LabelApi.java b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
new file mode 100644
index 0000000..975a57e
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.projects;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+
+public interface LabelApi {
+ LabelApi create(LabelDefinitionInput input) throws RestApiException;
+
+ LabelDefinitionInfo get() throws RestApiException;
+
+ LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException;
+
+ default void delete() throws RestApiException {
+ delete(null);
+ }
+
+ void delete(@Nullable String commitMessage) throws RestApiException;
+
+ /**
+ * A default implementation which allows source compatibility when adding new methods to the
+ * interface.
+ */
+ class NotImplemented implements LabelApi {
+ @Override
+ public LabelApi create(LabelDefinitionInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public LabelDefinitionInfo get() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public void delete(@Nullable String commitMessage) throws RestApiException {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
index c6d9dee..6d02ec4 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -19,6 +19,7 @@
import com.google.gerrit.extensions.api.config.AccessCheckInfo;
import com.google.gerrit.extensions.api.config.AccessCheckInput;
import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
@@ -204,6 +205,21 @@
/** Reindexes all changes of the project. */
void indexChanges() throws RestApiException;
+ ListLabelsRequest labels() throws RestApiException;
+
+ abstract class ListLabelsRequest {
+ protected boolean inherited;
+
+ public abstract List<LabelDefinitionInfo> get() throws RestApiException;
+
+ public ListLabelsRequest withInherited(boolean inherited) {
+ this.inherited = inherited;
+ return this;
+ }
+ }
+
+ LabelApi label(String labelName) throws RestApiException;
+
/**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
@@ -378,5 +394,15 @@
public void indexChanges() throws RestApiException {
throw new NotImplementedException();
}
+
+ @Override
+ public ListLabelsRequest labels() throws RestApiException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public LabelApi label(String labelName) throws RestApiException {
+ throw new NotImplementedException();
+ }
}
}
diff --git a/java/com/google/gerrit/extensions/common/InputWithCommitMessage.java b/java/com/google/gerrit/extensions/common/InputWithCommitMessage.java
new file mode 100644
index 0000000..3e96b30
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/InputWithCommitMessage.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import com.google.gerrit.common.Nullable;
+
+/** A generic input with a commit message only. */
+public class InputWithCommitMessage {
+ public String commitMessage;
+
+ public InputWithCommitMessage() {
+ this(null);
+ }
+
+ public InputWithCommitMessage(@Nullable String commitMessage) {
+ this.commitMessage = commitMessage;
+ }
+}
diff --git a/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java b/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java
new file mode 100644
index 0000000..64c3997
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/LabelDefinitionInfo.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.List;
+import java.util.Map;
+
+public class LabelDefinitionInfo {
+ public String name;
+ public String projectName;
+ public String function;
+ public Map<String, String> values;
+ public short defaultValue;
+ public List<String> branches;
+ public Boolean canOverride;
+ public Boolean copyAnyScore;
+ public Boolean copyMinScore;
+ public Boolean copyMaxScore;
+ public Boolean copyAllScoresIfNoChange;
+ public Boolean copyAllScoresIfNoCodeChange;
+ public Boolean copyAllScoresOnTrivialRebase;
+ public Boolean copyAllScoresOnMergeFirstParentUpdate;
+ public Boolean allowPostSubmit;
+ public Boolean ignoreSelfApproval;
+}
diff --git a/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java b/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java
new file mode 100644
index 0000000..6b088d3
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/LabelDefinitionInput.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.List;
+import java.util.Map;
+
+public class LabelDefinitionInput {
+ public String commitMessage;
+ public String name;
+ public String function;
+ public Map<String, String> values;
+ public Short defaultValue;
+ public List<String> branches;
+ public Boolean canOverride;
+ public Boolean copyAnyScore;
+ public Boolean copyMinScore;
+ public Boolean copyMaxScore;
+ public Boolean copyAllScoresIfNoChange;
+ public Boolean copyAllScoresIfNoCodeChange;
+ public Boolean copyAllScoresOnTrivialRebase;
+ public Boolean copyAllScoresOnMergeFirstParentUpdate;
+ public Boolean allowPostSubmit;
+ public Boolean ignoreSelfApproval;
+}
diff --git a/java/com/google/gerrit/extensions/common/RevertSubmissionInfo.java b/java/com/google/gerrit/extensions/common/RevertSubmissionInfo.java
new file mode 100644
index 0000000..dabd035
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/RevertSubmissionInfo.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.common;
+
+import java.util.List;
+
+public class RevertSubmissionInfo {
+ public List<ChangeInfo> revertChanges;
+}
diff --git a/java/com/google/gerrit/server/BUILD b/java/com/google/gerrit/server/BUILD
index 6675595..dbaf9c3 100644
--- a/java/com/google/gerrit/server/BUILD
+++ b/java/com/google/gerrit/server/BUILD
@@ -54,6 +54,7 @@
"//java/com/google/gerrit/prettify:server",
"//java/com/google/gerrit/proto",
"//java/com/google/gerrit/server/cache/serialize",
+ "//java/com/google/gerrit/server/git/receive:ref_cache",
"//java/com/google/gerrit/server/ioutil",
"//java/com/google/gerrit/server/logging",
"//java/com/google/gerrit/server/util/git",
diff --git a/java/com/google/gerrit/server/ChangeUtil.java b/java/com/google/gerrit/server/ChangeUtil.java
index ee82a26..a166d97 100644
--- a/java/com/google/gerrit/server/ChangeUtil.java
+++ b/java/com/google/gerrit/server/ChangeUtil.java
@@ -25,7 +25,6 @@
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Collection;
-import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.stream.Stream;
@@ -50,22 +49,6 @@
}
/**
- * Get the next patch set ID from a previously-read map of all refs.
- *
- * @param allRefs map of full ref name to ref.
- * @param id previous patch set ID.
- * @return next unused patch set ID for the same change, skipping any IDs whose corresponding ref
- * names appear in the {@code allRefs} map.
- */
- public static PatchSet.Id nextPatchSetIdFromAllRefsMap(Map<String, Ref> allRefs, PatchSet.Id id) {
- PatchSet.Id next = nextPatchSetId(id);
- while (allRefs.containsKey(next.toRefName())) {
- next = nextPatchSetId(next);
- }
- return next;
- }
-
- /**
* Get the next patch set ID from a previously-read map of refs below the change prefix.
*
* @param changeRefNames existing full change ref names with the same change ID as {@code id}.
@@ -95,9 +78,7 @@
/**
* Get the next patch set ID just looking at a single previous patch set ID.
*
- * <p>This patch set ID may or may not be available in the database; callers that want a
- * previously-unused ID should use {@link #nextPatchSetIdFromAllRefsMap} or {@link
- * #nextPatchSetIdFromChangeRefs}.
+ * <p>This patch set ID may or may not be available in the database.
*
* @param id previous patch set ID.
* @return next patch set ID for the same change, incrementing by 1.
diff --git a/java/com/google/gerrit/server/PublishCommentUtil.java b/java/com/google/gerrit/server/PublishCommentUtil.java
index 26539c5..c446c92 100644
--- a/java/com/google/gerrit/server/PublishCommentUtil.java
+++ b/java/com/google/gerrit/server/PublishCommentUtil.java
@@ -15,12 +15,13 @@
package com.google.gerrit.server;
import static com.google.common.base.Preconditions.checkArgument;
-import static com.google.gerrit.entities.Comment.Status;
import static java.util.stream.Collectors.toSet;
import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Comment;
+import com.google.gerrit.entities.Comment.Status;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.validators.CommentForValidation;
@@ -34,10 +35,14 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Collection;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
@Singleton
public class PublishCommentUtil {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
private final PatchListCache patchListCache;
private final PatchSetUtil psUtil;
private final CommentsUtil commentsUtil;
@@ -63,11 +68,32 @@
Map<PatchSet.Id, PatchSet> patchSets =
psUtil.getAsMap(notes, draftComments.stream().map(d -> psId(notes, d)).collect(toSet()));
+ Set<Comment> commentsToPublish = new HashSet<>();
for (Comment draftComment : draftComments) {
PatchSet.Id psIdOfDraftComment = psId(notes, draftComment);
PatchSet ps = patchSets.get(psIdOfDraftComment);
if (ps == null) {
- throw new StorageException("patch set " + psIdOfDraftComment + " not found");
+ // This can happen if changes with the same numeric ID exist:
+ // - change 12345 has 3 patch sets in repo X
+ // - another change 12345 has 7 patch sets in repo Y
+ // - the user saves a draft comment on patch set 6 of the change in repo Y
+ // - this draft comment gets stored in:
+ // AllUsers -> refs/draft-comments/45/12345/<account-id>
+ // - when posting a review with draft handling PUBLISH_ALL_REVISIONS on the change in
+ // repo X, the draft comments are loaded from
+ // AllUsers -> refs/draft-comments/45/12345/<account-id>, including the draft
+ // comment that was saved for patch set 6 of the change in repo Y
+ // - patch set 6 does not exist for the change in repo x, hence we get null for the patch
+ // set here
+ // Instead of failing hard (and returning an Internal Server Error) to the caller,
+ // just ignore that comment.
+ // Gerrit ensures that numeric change IDs are unique, but you can get duplicates if
+ // change refs of one repo are copied/pushed to another repo on the same host (this
+ // should never be done, but we know it happens).
+ logger.atWarning().log(
+ "Ignoring draft comment %s on non existing patch set %s (repo = %s)",
+ draftComment, psIdOfDraftComment, notes.getProjectName());
+ continue;
}
draftComment.writtenOn = ctx.getWhen();
draftComment.tag = tag;
@@ -79,8 +105,9 @@
} catch (PatchListNotAvailableException e) {
throw new StorageException(e);
}
+ commentsToPublish.add(draftComment);
}
- commentsUtil.putComments(ctx.getUpdate(psId), Status.PUBLISHED, draftComments);
+ commentsUtil.putComments(ctx.getUpdate(psId), Status.PUBLISHED, commentsToPublish);
}
private static PatchSet.Id psId(ChangeNotes notes, Comment c) {
diff --git a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index a04be30..d0af686 100644
--- a/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -50,6 +50,7 @@
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.common.MergePatchSetInput;
import com.google.gerrit.extensions.common.PureRevertInfo;
+import com.google.gerrit.extensions.common.RevertSubmissionInfo;
import com.google.gerrit.extensions.common.RobotCommentInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
import com.google.gerrit.extensions.registration.DynamicMap;
@@ -96,6 +97,7 @@
import com.google.gerrit.server.restapi.change.Rebase;
import com.google.gerrit.server.restapi.change.Restore;
import com.google.gerrit.server.restapi.change.Revert;
+import com.google.gerrit.server.restapi.change.RevertSubmission;
import com.google.gerrit.server.restapi.change.Reviewers;
import com.google.gerrit.server.restapi.change.Revisions;
import com.google.gerrit.server.restapi.change.SetReadyForReview;
@@ -132,6 +134,7 @@
private final ChangeResource change;
private final Abandon abandon;
private final Revert revert;
+ private final RevertSubmission revertSubmission;
private final Restore restore;
private final CreateMergePatchSet updateByMerge;
private final Provider<SubmittedTogether> submittedTogether;
@@ -181,6 +184,7 @@
ListReviewers listReviewers,
Abandon abandon,
Revert revert,
+ RevertSubmission revertSubmission,
Restore restore,
CreateMergePatchSet updateByMerge,
Provider<SubmittedTogether> submittedTogether,
@@ -219,6 +223,7 @@
@Assisted ChangeResource change) {
this.changeApi = changeApi;
this.revert = revert;
+ this.revertSubmission = revertSubmission;
this.reviewers = reviewers;
this.revisions = revisions;
this.reviewerApi = reviewerApi;
@@ -358,6 +363,15 @@
}
@Override
+ public RevertSubmissionInfo revertSubmission(RevertInput in) throws RestApiException {
+ try {
+ return revertSubmission.apply(change, in).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot revert a change submission", e);
+ }
+ }
+
+ @Override
public ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException {
try {
return updateByMerge.apply(change, in).value();
diff --git a/java/com/google/gerrit/server/api/projects/LabelApiImpl.java b/java/com/google/gerrit/server/api/projects/LabelApiImpl.java
new file mode 100644
index 0000000..ad7ec31
--- /dev/null
+++ b/java/com/google/gerrit/server/api/projects/LabelApiImpl.java
@@ -0,0 +1,118 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.api.projects;
+
+import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.projects.LabelApi;
+import com.google.gerrit.extensions.common.InputWithCommitMessage;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.LabelResource;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.restapi.project.CreateLabel;
+import com.google.gerrit.server.restapi.project.DeleteLabel;
+import com.google.gerrit.server.restapi.project.GetLabel;
+import com.google.gerrit.server.restapi.project.LabelsCollection;
+import com.google.gerrit.server.restapi.project.SetLabel;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class LabelApiImpl implements LabelApi {
+ interface Factory {
+ LabelApiImpl create(ProjectResource project, String label);
+ }
+
+ private final LabelsCollection labels;
+ private final CreateLabel createLabel;
+ private final GetLabel getLabel;
+ private final SetLabel setLabel;
+ private final DeleteLabel deleteLabel;
+ private final ProjectCache projectCache;
+ private final String label;
+
+ private ProjectResource project;
+
+ @Inject
+ LabelApiImpl(
+ LabelsCollection labels,
+ CreateLabel createLabel,
+ GetLabel getLabel,
+ SetLabel setLabel,
+ DeleteLabel deleteLabel,
+ ProjectCache projectCache,
+ @Assisted ProjectResource project,
+ @Assisted String label) {
+ this.labels = labels;
+ this.createLabel = createLabel;
+ this.getLabel = getLabel;
+ this.setLabel = setLabel;
+ this.deleteLabel = deleteLabel;
+ this.projectCache = projectCache;
+ this.project = project;
+ this.label = label;
+ }
+
+ @Override
+ public LabelApi create(LabelDefinitionInput input) throws RestApiException {
+ try {
+ createLabel.apply(project, IdString.fromDecoded(label), input);
+
+ // recreate project resource because project state was updated by creating the new label and
+ // needs to be reloaded
+ project =
+ new ProjectResource(projectCache.checkedGet(project.getNameKey()), project.getUser());
+ return this;
+ } catch (Exception e) {
+ throw asRestApiException("Cannot create branch", e);
+ }
+ }
+
+ @Override
+ public LabelDefinitionInfo get() throws RestApiException {
+ try {
+ return getLabel.apply(resource()).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot get label", e);
+ }
+ }
+
+ @Override
+ public LabelDefinitionInfo update(LabelDefinitionInput input) throws RestApiException {
+ try {
+ return setLabel.apply(resource(), input).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot update label", e);
+ }
+ }
+
+ @Override
+ public void delete(@Nullable String commitMessage) throws RestApiException {
+ try {
+ deleteLabel.apply(resource(), new InputWithCommitMessage(commitMessage));
+ } catch (Exception e) {
+ throw asRestApiException("Cannot delete label", e);
+ }
+ }
+
+ private LabelResource resource() throws RestApiException, PermissionBackendException {
+ return labels.parse(project, IdString.fromDecoded(label));
+ }
+}
diff --git a/java/com/google/gerrit/server/api/projects/Module.java b/java/com/google/gerrit/server/api/projects/Module.java
index f1e21d28..8df5495 100644
--- a/java/com/google/gerrit/server/api/projects/Module.java
+++ b/java/com/google/gerrit/server/api/projects/Module.java
@@ -28,5 +28,6 @@
factory(ChildProjectApiImpl.Factory.class);
factory(CommitApiImpl.Factory.class);
factory(DashboardApiImpl.Factory.class);
+ factory(LabelApiImpl.Factory.class);
}
}
diff --git a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
index 1ac905d..d7ab91b 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -37,6 +37,7 @@
import com.google.gerrit.extensions.api.projects.DescriptionInput;
import com.google.gerrit.extensions.api.projects.HeadInput;
import com.google.gerrit.extensions.api.projects.IndexProjectInput;
+import com.google.gerrit.extensions.api.projects.LabelApi;
import com.google.gerrit.extensions.api.projects.ParentInput;
import com.google.gerrit.extensions.api.projects.ProjectApi;
import com.google.gerrit.extensions.api.projects.ProjectInput;
@@ -44,6 +45,7 @@
import com.google.gerrit.extensions.api.projects.TagInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.Input;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.IdString;
@@ -73,6 +75,7 @@
import com.google.gerrit.server.restapi.project.IndexChanges;
import com.google.gerrit.server.restapi.project.ListBranches;
import com.google.gerrit.server.restapi.project.ListDashboards;
+import com.google.gerrit.server.restapi.project.ListLabels;
import com.google.gerrit.server.restapi.project.ListTags;
import com.google.gerrit.server.restapi.project.ProjectsCollection;
import com.google.gerrit.server.restapi.project.PutConfig;
@@ -127,6 +130,8 @@
private final SetParent setParent;
private final Index index;
private final IndexChanges indexChanges;
+ private final Provider<ListLabels> listLabels;
+ private final LabelApiImpl.Factory labelApi;
@AssistedInject
ProjectApiImpl(
@@ -162,6 +167,8 @@
SetParent setParent,
Index index,
IndexChanges indexChanges,
+ Provider<ListLabels> listLabels,
+ LabelApiImpl.Factory labelApi,
@Assisted ProjectResource project) {
this(
permissionBackend,
@@ -197,6 +204,8 @@
setParent,
index,
indexChanges,
+ listLabels,
+ labelApi,
null);
}
@@ -234,6 +243,8 @@
SetParent setParent,
Index index,
IndexChanges indexChanges,
+ Provider<ListLabels> listLabels,
+ LabelApiImpl.Factory labelApi,
@Assisted String name) {
this(
permissionBackend,
@@ -269,6 +280,8 @@
setParent,
index,
indexChanges,
+ listLabels,
+ labelApi,
name);
}
@@ -306,6 +319,8 @@
SetParent setParent,
Index index,
IndexChanges indexChanges,
+ Provider<ListLabels> listLabels,
+ LabelApiImpl.Factory labelApi,
String name) {
this.permissionBackend = permissionBackend;
this.createProject = createProject;
@@ -341,6 +356,8 @@
this.name = name;
this.index = index;
this.indexChanges = indexChanges;
+ this.listLabels = listLabels;
+ this.labelApi = labelApi;
}
@Override
@@ -672,4 +689,27 @@
}
return project;
}
+
+ @Override
+ public ListLabelsRequest labels() {
+ return new ListLabelsRequest() {
+ @Override
+ public List<LabelDefinitionInfo> get() throws RestApiException {
+ try {
+ return listLabels.get().withInherited(inherited).apply(checkExists()).value();
+ } catch (Exception e) {
+ throw asRestApiException("Cannot list labels", e);
+ }
+ }
+ };
+ }
+
+ @Override
+ public LabelApi label(String labelName) throws RestApiException {
+ try {
+ return labelApi.create(checkExists(), labelName);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot parse label", e);
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index afaf695..c05a47d 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -17,12 +17,14 @@
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MergeConflictException;
import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
@@ -228,7 +230,12 @@
createCommit(repository, basePatchSetCommit, baseTree, newCommitMessage, nowTimestamp);
if (optionalChangeEdit.isPresent()) {
- updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp);
+ updateEdit(
+ notes.getProjectName(),
+ repository,
+ optionalChangeEdit.get(),
+ newEditCommit,
+ nowTimestamp);
} else {
createEdit(repository, notes, basePatchSet, newEditCommit, nowTimestamp);
}
@@ -331,7 +338,12 @@
createCommit(repository, basePatchSetCommit, newTreeId, commitMessage, nowTimestamp);
if (optionalChangeEdit.isPresent()) {
- updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp);
+ updateEdit(
+ notes.getProjectName(),
+ repository,
+ optionalChangeEdit.get(),
+ newEditCommit,
+ nowTimestamp);
} else {
createEdit(repository, notes, basePatchSet, newEditCommit, nowTimestamp);
}
@@ -384,7 +396,12 @@
createCommit(repository, patchSetCommit, newTreeId, commitMessage, nowTimestamp);
if (optionalChangeEdit.isPresent()) {
- return updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp);
+ return updateEdit(
+ notes.getProjectName(),
+ repository,
+ optionalChangeEdit.get(),
+ newEditCommit,
+ nowTimestamp);
}
return createEdit(repository, notes, patchSet, newEditCommit, nowTimestamp);
}
@@ -531,7 +548,13 @@
throws IOException {
Change change = notes.getChange();
String editRefName = getEditRefName(change, basePatchSet);
- updateReference(repository, editRefName, ObjectId.zeroId(), newEditCommitId, timestamp);
+ updateReference(
+ notes.getProjectName(),
+ repository,
+ editRefName,
+ ObjectId.zeroId(),
+ newEditCommitId,
+ timestamp);
reindex(change);
RevCommit newEditCommit = lookupCommit(repository, newEditCommitId);
@@ -544,11 +567,16 @@
}
private ChangeEdit updateEdit(
- Repository repository, ChangeEdit changeEdit, ObjectId newEditCommitId, Timestamp timestamp)
+ Project.NameKey projectName,
+ Repository repository,
+ ChangeEdit changeEdit,
+ ObjectId newEditCommitId,
+ Timestamp timestamp)
throws IOException {
String editRefName = changeEdit.getRefName();
RevCommit currentEditCommit = changeEdit.getEditCommit();
- updateReference(repository, editRefName, currentEditCommit, newEditCommitId, timestamp);
+ updateReference(
+ projectName, repository, editRefName, currentEditCommit, newEditCommitId, timestamp);
reindex(changeEdit.getChange());
RevCommit newEditCommit = lookupCommit(repository, newEditCommitId);
@@ -557,6 +585,7 @@
}
private void updateReference(
+ Project.NameKey projectName,
Repository repository,
String refName,
ObjectId currentObjectId,
@@ -571,14 +600,12 @@
ru.setForceUpdate(true);
try (RevWalk revWalk = new RevWalk(repository)) {
RefUpdate.Result res = ru.update(revWalk);
+ String message = "cannot update " + ru.getName() + " in " + projectName + ": " + res;
+ if (res == RefUpdate.Result.LOCK_FAILURE) {
+ throw new LockFailureException(message, ru);
+ }
if (res != RefUpdate.Result.NEW && res != RefUpdate.Result.FORCED) {
- throw new IOException(
- "cannot update "
- + ru.getName()
- + " in "
- + repository.getDirectory()
- + ": "
- + ru.getResult());
+ throw new IOException(message);
}
}
}
diff --git a/java/com/google/gerrit/server/git/DelegateRefDatabase.java b/java/com/google/gerrit/server/git/DelegateRefDatabase.java
index 34dd6a9..decae05 100644
--- a/java/com/google/gerrit/server/git/DelegateRefDatabase.java
+++ b/java/com/google/gerrit/server/git/DelegateRefDatabase.java
@@ -17,6 +17,9 @@
import java.io.IOException;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.RefRename;
@@ -24,7 +27,8 @@
import org.eclipse.jgit.lib.Repository;
/**
- * Wrapper around {@link RefDatabase} that delegates all calls to the wrapped {@link RefDatabase}.
+ * Wrapper around {@link RefDatabase} that delegates all calls to the wrapped {@link Repository}'s
+ * {@link RefDatabase}.
*/
public class DelegateRefDatabase extends RefDatabase {
@@ -41,7 +45,7 @@
@Override
public void close() {
- delegate.close();
+ delegate.getRefDatabase().close();
}
@Override
@@ -71,6 +75,12 @@
}
@Override
+ @NonNull
+ public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException {
+ return delegate.getRefDatabase().getTipsWithSha1(id);
+ }
+
+ @Override
public List<Ref> getAdditionalRefs() throws IOException {
return delegate.getRefDatabase().getAdditionalRefs();
}
diff --git a/java/com/google/gerrit/server/git/GroupCollector.java b/java/com/google/gerrit/server/git/GroupCollector.java
index c284f7f4..1f0dcd4 100644
--- a/java/com/google/gerrit/server/git/GroupCollector.java
+++ b/java/com/google/gerrit/server/git/GroupCollector.java
@@ -23,16 +23,18 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.common.collect.SortedSetMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.git.receive.ReceivePackRefCache;
import com.google.gerrit.server.notedb.ChangeNotes;
+import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
@@ -87,24 +89,29 @@
return rsrc.getPatchSet().groups();
}
- private interface Lookup {
+ interface Lookup {
List<String> lookup(PatchSet.Id psId);
}
- private final ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha;
+ private final ReceivePackRefCache receivePackRefCache;
private final ListMultimap<ObjectId, String> groups;
private final SetMultimap<String, String> groupAliases;
private final Lookup groupLookup;
private boolean done;
+ /**
+ * Returns a new {@link GroupCollector} instance.
+ *
+ * @see GroupCollector for what this class does.
+ */
public static GroupCollector create(
- ListMultimap<ObjectId, Ref> changeRefsById,
+ ReceivePackRefCache receivePackRefCache,
PatchSetUtil psUtil,
ChangeNotes.Factory notesFactory,
Project.NameKey project) {
return new GroupCollector(
- transformRefs(changeRefsById),
+ receivePackRefCache,
psId -> {
// TODO(dborowitz): Reuse open repository from caller.
ChangeNotes notes = notesFactory.createChecked(project, psId.changeId());
@@ -113,31 +120,32 @@
});
}
- private GroupCollector(ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha, Lookup groupLookup) {
- this.patchSetsBySha = patchSetsBySha;
+ /**
+ * Returns a new {@link GroupCollector} instance.
+ *
+ * <p>Used in production code by using {@link com.google.gerrit.server.notedb.ChangeNotes.Factory}
+ * to get a group SHA1 (40 bytes string representation) from a {@link
+ * com.google.gerrit.entities.PatchSet.Id}. Unit tests use this method directly by passing their
+ * own lookup function.
+ *
+ * @see GroupCollector for what this class does.
+ */
+ @VisibleForTesting
+ GroupCollector(ReceivePackRefCache receivePackRefCache, Lookup groupLookup) {
+ this.receivePackRefCache = receivePackRefCache;
this.groupLookup = groupLookup;
groups = MultimapBuilder.hashKeys().arrayListValues().build();
groupAliases = MultimapBuilder.hashKeys().hashSetValues().build();
}
- private static ListMultimap<ObjectId, PatchSet.Id> transformRefs(
- ListMultimap<ObjectId, Ref> refs) {
- return Multimaps.transformValues(refs, r -> PatchSet.Id.fromRef(r.getName()));
- }
-
- @VisibleForTesting
- GroupCollector(
- ListMultimap<ObjectId, PatchSet.Id> patchSetsBySha,
- ListMultimap<PatchSet.Id, String> groupLookup) {
- this(
- patchSetsBySha,
- psId -> {
- List<String> groups = groupLookup.get(psId);
- return !groups.isEmpty() ? groups : null;
- });
- }
-
- public void visit(RevCommit c) {
+ /**
+ * Process the given {@link RevCommit}. Callers must call {@link #visit(RevCommit)} on all commits
+ * between the current branch tip and the tip of a push, in reverse topo order (parents before
+ * children). Once all commits have been visited, call {@link #getGroups()} for the result.
+ *
+ * @see GroupCollector for what this class does.
+ */
+ public void visit(RevCommit c) throws IOException {
checkState(!done, "visit() called after getGroups()");
Set<RevCommit> interestingParents = getInterestingParents(c);
@@ -197,7 +205,10 @@
}
}
- public SortedSetMultimap<ObjectId, String> getGroups() {
+ /**
+ * Returns the groups that got collected from visiting commits using {@link #visit(RevCommit)}.
+ */
+ public SortedSetMultimap<ObjectId, String> getGroups() throws IOException {
done = true;
SortedSetMultimap<ObjectId, String> result =
MultimapBuilder.hashKeys(groups.keySet().size()).treeSetValues().build();
@@ -218,12 +229,13 @@
return result;
}
- private boolean isGroupFromExistingPatchSet(RevCommit commit, String group) {
+ private boolean isGroupFromExistingPatchSet(RevCommit commit, String group) throws IOException {
ObjectId id = parseGroup(commit, group);
- return id != null && patchSetsBySha.containsKey(id);
+ return id != null && !receivePackRefCache.tipsFromObjectId(id, RefNames.REFS_CHANGES).isEmpty();
}
- private Set<String> resolveGroups(ObjectId forCommit, Collection<String> candidates) {
+ private Set<String> resolveGroups(ObjectId forCommit, Collection<String> candidates)
+ throws IOException {
Set<String> actual = Sets.newTreeSet();
Set<String> done = Sets.newHashSetWithExpectedSize(candidates.size());
Set<String> seen = Sets.newHashSetWithExpectedSize(candidates.size());
@@ -258,16 +270,20 @@
}
}
- private Iterable<String> resolveGroup(ObjectId forCommit, String group) {
+ private Iterable<String> resolveGroup(ObjectId forCommit, String group) throws IOException {
ObjectId id = parseGroup(forCommit, group);
if (id != null) {
- PatchSet.Id psId = Iterables.getFirst(patchSetsBySha.get(id), null);
- if (psId != null) {
- List<String> groups = groupLookup.lookup(psId);
- // Group for existing patch set may be missing, e.g. if group has not
- // been migrated yet.
- if (groups != null && !groups.isEmpty()) {
- return groups;
+ Ref ref =
+ Iterables.getFirst(receivePackRefCache.tipsFromObjectId(id, RefNames.REFS_CHANGES), null);
+ if (ref != null) {
+ PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
+ if (psId != null) {
+ List<String> groups = groupLookup.lookup(psId);
+ // Group for existing patch set may be missing, e.g. if group has not
+ // been migrated yet.
+ if (groups != null && !groups.isEmpty()) {
+ return groups;
+ }
}
}
}
diff --git a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
index 8f7e684..8421e54 100644
--- a/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
+++ b/java/com/google/gerrit/server/git/PermissionAwareReadOnlyRefDatabase.java
@@ -26,10 +26,13 @@
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefRename;
import org.eclipse.jgit.lib.RefUpdate;
@@ -156,4 +159,17 @@
}
return null;
}
+
+ @Override
+ @NonNull
+ public Set<Ref> getTipsWithSha1(ObjectId id) throws IOException {
+ Set<Ref> unfiltered = super.getTipsWithSha1(id);
+ Set<Ref> result = new HashSet<>(unfiltered.size());
+ for (Ref ref : unfiltered) {
+ if (exactRef(ref.getName()) != null) {
+ result.add(ref);
+ }
+ }
+ return result;
+ }
}
diff --git a/java/com/google/gerrit/server/git/receive/BUILD b/java/com/google/gerrit/server/git/receive/BUILD
index d89bb63..2b04d4d 100644
--- a/java/com/google/gerrit/server/git/receive/BUILD
+++ b/java/com/google/gerrit/server/git/receive/BUILD
@@ -2,9 +2,13 @@
java_library(
name = "receive",
- srcs = glob(["**/*.java"]),
+ srcs = glob(
+ ["**/*.java"],
+ exclude = ["ReceivePackRefCache.java"],
+ ),
visibility = ["//visibility:public"],
deps = [
+ ":ref_cache",
"//java/com/google/gerrit/common:annotations",
"//java/com/google/gerrit/common:server",
"//java/com/google/gerrit/entities",
@@ -26,3 +30,14 @@
"//lib/guice:guice-assistedinject",
],
)
+
+java_library(
+ name = "ref_cache",
+ srcs = glob(["ReceivePackRefCache.java"]),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//java/com/google/gerrit/entities",
+ "//lib:guava",
+ "//lib:jgit",
+ ],
+)
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index c6c9b39..7dd21e1 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -343,7 +343,6 @@
private final SetPrivateOp.Factory setPrivateOpFactory;
// Assisted injected fields.
- private final AllRefsWatcher allRefsWatcher;
private final ProjectState projectState;
private final IdentifiedUser user;
private final ReceivePack receivePack;
@@ -363,12 +362,9 @@
private final ListMultimap<String, String> errors;
private final ListMultimap<String, String> pushOptions;
+ private final ReceivePackRefCache receivePackRefCache;
private final Map<Change.Id, ReplaceRequest> replaceByChange;
- // Collections lazily populated during processing.
- private ListMultimap<Change.Id, Ref> refsByChange;
- private ListMultimap<ObjectId, Ref> refsById;
-
// Other settings populated during processing.
private MagicBranchInput magicBranch;
private boolean newChangeForAllNotInTarget;
@@ -469,7 +465,6 @@
this.setPrivateOpFactory = setPrivateOpFactory;
// Assisted injected fields.
- this.allRefsWatcher = allRefsWatcher;
this.projectState = projectState;
this.user = user;
this.receivePack = rp;
@@ -501,6 +496,13 @@
this.messageSender = messageSender != null ? messageSender : new ReceivePackMessageSender();
this.resultChangeIds = resultChangeIds;
this.loggingTags = ImmutableMap.of();
+
+ // TODO(hiesel): Make this decision implicit once vetted
+ boolean useRefCache = config.getBoolean("receive", "enableInMemoryRefCache", true);
+ receivePackRefCache =
+ useRefCache
+ ? ReceivePackRefCache.withAdvertisedRefs(() -> allRefsWatcher.getAllRefs())
+ : ReceivePackRefCache.noCache(receivePack.getRepository().getRefDatabase());
}
void init() {
@@ -653,17 +655,27 @@
Task replaceProgress = progress.beginSubTask("updated", UNKNOWN);
List<CreateRequest> newChanges = Collections.emptyList();
- if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
- newChanges = selectNewAndReplacedChangesFromMagicBranch(newProgress);
+ try {
+ if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
+ try {
+ newChanges = selectNewAndReplacedChangesFromMagicBranch(newProgress);
+ } catch (IOException e) {
+ logger.atSevere().withCause(e).log(
+ "Failed to select new changes in %s", project.getName());
+ return;
+ }
+ }
+
+ // Commit validation has already happened, so any changes without Change-Id are for the
+ // deprecated feature.
+ warnAboutMissingChangeId(newChanges);
+ preparePatchSetsForReplace(newChanges);
+ insertChangesAndPatchSets(newChanges, replaceProgress);
+ } finally {
+ newProgress.end();
+ replaceProgress.end();
}
- // Commit validation has already happened, so any changes without Change-Id are for the
- // deprecated feature.
- warnAboutMissingChangeId(newChanges);
- preparePatchSetsForReplace(newChanges);
- insertChangesAndPatchSets(newChanges, replaceProgress);
- newProgress.end();
- replaceProgress.end();
queueSuccessMessages(newChanges);
logger.atFine().log(
@@ -1644,8 +1656,9 @@
/**
* returns the destination ref of the magic branch, and populates options in the cmdLineParser.
*/
- String parse(Repository repo, Set<String> refs, ListMultimap<String, String> pushOptions)
- throws CmdLineException {
+ String parse(
+ Repository repo, ReceivePackRefCache refCache, ListMultimap<String, String> pushOptions)
+ throws CmdLineException, IOException {
String ref = RefNames.fullName(MagicBranch.getDestBranchName(cmd.getRefName()));
ListMultimap<String, String> options = LinkedListMultimap.create(pushOptions);
@@ -1675,7 +1688,7 @@
int split = ref.length();
for (; ; ) {
String name = ref.substring(0, split);
- if (refs.contains(name) || name.equals(head)) {
+ if (refCache.exactRef(name) != null || name.equals(head)) {
break;
}
@@ -1734,7 +1747,7 @@
*
* <p>Assumes we are handling a magic branch here.
*/
- private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException {
+ private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException, IOException {
try (TraceTimer traceTimer = newTimer("parseMagicBranch")) {
logger.atFine().log("Found magic branch %s", cmd.getRefName());
MagicBranchInput magicBranch = new MagicBranchInput(user, projectState, cmd, labelTypes);
@@ -1743,7 +1756,7 @@
magicBranch.cmdLineParser = optionParserFactory.create(magicBranch);
try {
- ref = magicBranch.parse(repo, receivePack.getAdvertisedRefs().keySet(), pushOptions);
+ ref = magicBranch.parse(repo, receivePackRefCache, pushOptions);
} catch (CmdLineException e) {
if (!magicBranch.cmdLineParser.wasHelpRequestedByOption()) {
logger.atFine().log("Invalid branch syntax");
@@ -1775,7 +1788,7 @@
// review to these branches is allowed even if the branch does not exist yet. This allows to
// push initial code for review to an empty repository and to review an initial project
// configuration.
- if (!receivePack.getAdvertisedRefs().containsKey(ref)
+ if (receivePackRefCache.exactRef(ref) == null
&& !ref.equals(readHEAD(repo))
&& !ref.equals(RefNames.REFS_CONFIG)) {
logger.atFine().log("Ref %s not found", ref);
@@ -1850,11 +1863,12 @@
reject(cmd, "cannot use merged with base");
return;
}
- RevCommit branchTip = readBranchTip(magicBranch.dest);
- if (branchTip == null) {
+ Ref refTip = receivePackRefCache.exactRef(magicBranch.dest.branch());
+ if (refTip == null) {
reject(cmd, magicBranch.dest.branch() + " not found");
return;
}
+ RevCommit branchTip = receivePack.getRevWalk().parseCommit(refTip.getObjectId());
if (!walk.isMergedInto(tip, branchTip)) {
reject(cmd, "not merged into branch");
return;
@@ -1891,8 +1905,9 @@
}
}
} else if (newChangeForAllNotInTarget) {
- RevCommit branchTip = readBranchTip(magicBranch.dest);
- if (branchTip != null) {
+ Ref refTip = receivePackRefCache.exactRef(magicBranch.dest.branch());
+ if (refTip != null) {
+ RevCommit branchTip = receivePack.getRevWalk().parseCommit(refTip.getObjectId());
magicBranch.baseCommit = Collections.singletonList(branchTip);
logger.atFine().log("Set baseCommit = %s", magicBranch.baseCommit.get(0).name());
} else {
@@ -1939,7 +1954,7 @@
newTimer("validateConnected", Metadata.builder().branchName(dest.branch()))) {
RevWalk walk = receivePack.getRevWalk();
try {
- Ref targetRef = receivePack.getAdvertisedRefs().get(dest.branch());
+ Ref targetRef = receivePackRefCache.exactRef(dest.branch());
if (targetRef == null || targetRef.getObjectId() == null) {
// The destination branch does not yet exist. Assume the
// history being sent for review will start it and thus
@@ -1986,14 +2001,6 @@
}
}
- private RevCommit readBranchTip(BranchNameKey branch) throws IOException {
- Ref r = allRefs().get(branch.branch());
- if (r == null) {
- return null;
- }
- return receivePack.getRevWalk().parseCommit(r.getObjectId());
- }
-
/**
* Update an existing change. If draft comments are to be published, these are validated and may
* be withheld.
@@ -2001,7 +2008,8 @@
* @return True if the command succeeded, false if it was rejected.
*/
private boolean requestReplaceAndValidateComments(
- ReceiveCommand cmd, boolean checkMergedInto, Change change, RevCommit newCommit) {
+ ReceiveCommand cmd, boolean checkMergedInto, Change change, RevCommit newCommit)
+ throws IOException {
try (TraceTimer traceTimer = newTimer("requestReplaceAndValidateComments")) {
if (change.isClosed()) {
reject(
@@ -2063,14 +2071,14 @@
}
}
- private List<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(Task newProgress) {
+ private List<CreateRequest> selectNewAndReplacedChangesFromMagicBranch(Task newProgress)
+ throws IOException {
try (TraceTimer traceTimer = newTimer("selectNewAndReplacedChangesFromMagicBranch")) {
logger.atFine().log("Finding new and replaced changes");
List<CreateRequest> newChanges = new ArrayList<>();
- ListMultimap<ObjectId, Ref> existing = changeRefsById();
GroupCollector groupCollector =
- GroupCollector.create(changeRefsById(), psUtil, notesFactory, project.getNameKey());
+ GroupCollector.create(receivePackRefCache, psUtil, notesFactory, project.getNameKey());
BranchCommitValidator validator =
commitValidatorFactory.create(projectState, magicBranch.dest, user);
@@ -2111,7 +2119,8 @@
receivePack.getRevWalk().parseBody(c);
String name = c.name();
groupCollector.visit(c);
- Collection<Ref> existingRefs = existing.get(c);
+ Collection<Ref> existingRefs =
+ receivePackRefCache.tipsFromObjectId(c, RefNames.REFS_CHANGES);
if (rejectImplicitMerges) {
Collections.addAll(mergedParents, c.getParents());
@@ -2275,7 +2284,8 @@
// In case the change look up from the index failed,
// double check against the existing refs
- if (foundInExistingRef(existing.get(p.commit))) {
+ if (foundInExistingRef(
+ receivePackRefCache.tipsFromObjectId(p.commit, RefNames.REFS_CHANGES))) {
if (pending.size() == 1) {
reject(magicBranch.cmd, "commit(s) already exists (as current patchset)");
return Collections.emptyList();
@@ -2382,7 +2392,7 @@
for (RevCommit c : magicBranch.baseCommit) {
receivePack.getRevWalk().markUninteresting(c);
}
- Ref targetRef = allRefs().get(magicBranch.dest.branch());
+ Ref targetRef = receivePackRefCache.exactRef(magicBranch.dest.branch());
if (targetRef != null) {
logger.atFine().log(
"Marking target ref %s (%s) uninteresting",
@@ -2397,7 +2407,7 @@
private void rejectImplicitMerges(Set<RevCommit> mergedParents) throws IOException {
try (TraceTimer traceTimer = newTimer("rejectImplicitMerges")) {
if (!mergedParents.isEmpty()) {
- Ref targetRef = allRefs().get(magicBranch.dest.branch());
+ Ref targetRef = receivePackRefCache.exactRef(magicBranch.dest.branch());
if (targetRef != null) {
RevWalk rw = receivePack.getRevWalk();
RevCommit tip = rw.parseCommit(targetRef.getObjectId());
@@ -2432,13 +2442,15 @@
// Mark all branch tips as uninteresting in the given revwalk,
// so we get only the new commits when walking rw.
- private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) {
+ private void markHeadsAsUninteresting(RevWalk rw, @Nullable String forRef) throws IOException {
try (TraceTimer traceTimer =
newTimer("markHeadsAsUninteresting", Metadata.builder().branchName(forRef))) {
int i = 0;
- for (Ref ref : allRefs().values()) {
- if ((ref.getName().startsWith(R_HEADS) || ref.getName().equals(forRef))
- && ref.getObjectId() != null) {
+ for (Ref ref :
+ Iterables.concat(
+ receivePackRefCache.byPrefix(R_HEADS),
+ Collections.singletonList(receivePackRefCache.exactRef(forRef)))) {
+ if (ref != null && ref.getObjectId() != null) {
try {
rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
i++;
@@ -2703,7 +2715,8 @@
ReplaceOp replaceOp;
ReplaceRequest(
- Change.Id toChange, RevCommit newCommit, ReceiveCommand cmd, boolean checkMergedInto) {
+ Change.Id toChange, RevCommit newCommit, ReceiveCommand cmd, boolean checkMergedInto)
+ throws IOException {
this.ontoChange = toChange;
this.newCommitId = newCommit.copy();
this.inputCommand = requireNonNull(cmd);
@@ -2715,11 +2728,12 @@
revCommit = null;
}
revisions = HashBiMap.create();
- for (Ref ref : refs(toChange)) {
+ for (Ref ref : receivePackRefCache.byPrefix(RefNames.changeRefPrefix(toChange))) {
try {
- revisions.forcePut(
- receivePack.getRevWalk().parseCommit(ref.getObjectId()),
- PatchSet.Id.fromRef(ref.getName()));
+ PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
+ if (psId != null) {
+ revisions.forcePut(receivePack.getRevWalk().parseCommit(ref.getObjectId()), psId);
+ }
} catch (IOException err) {
logger.atWarning().withCause(err).log(
"Project %s contains invalid change ref %s", project.getName(), ref.getName());
@@ -2785,6 +2799,16 @@
Change change = notes.getChange();
priorPatchSet = change.currentPatchSetId();
if (!revisions.containsValue(priorPatchSet)) {
+ logger.atWarning().log(
+ "Change %d is missing revision for patch set %s"
+ + " (it has revisions for these patch sets: %s)",
+ change.getChangeId(),
+ priorPatchSet.getId(),
+ Iterables.toString(
+ revisions.values().stream()
+ .limit(100) // Enough for "normal" changes.
+ .map(PatchSet.Id::getId)
+ .collect(Collectors.toList())));
reject(inputCommand, "change " + ontoChange + " missing revisions");
return false;
}
@@ -2812,11 +2836,16 @@
return false;
}
- for (Ref r : receivePack.getRepository().getRefDatabase().getRefsByPrefix("refs/changes")) {
- if (r.getObjectId().equals(newCommit)) {
- reject(inputCommand, "commit already exists (in the project)");
- return false;
- }
+ List<Ref> existingChangesWithSameCommit =
+ receivePackRefCache.tipsFromObjectId(newCommit, RefNames.REFS_CHANGES);
+ if (!existingChangesWithSameCommit.isEmpty()) {
+ // TODO(hiesel, hanwen): Remove this check entirely when Gerrit requires change IDs
+ // without the option to turn that off.
+ reject(
+ inputCommand,
+ "commit already exists (in the project): "
+ + existingChangesWithSameCommit.get(0).getName());
+ return false;
}
try (TraceTimer traceTimer2 = newTimer("validateNewPatchSetNoteDb#isMergedInto")) {
@@ -2950,14 +2979,20 @@
private void newPatchSet() throws IOException {
try (TraceTimer traceTimer = newTimer("newPatchSet")) {
RevCommit newCommit = receivePack.getRevWalk().parseCommit(newCommitId);
- psId =
- ChangeUtil.nextPatchSetIdFromAllRefsMap(
- allRefs(), notes.getChange().currentPatchSetId());
+ psId = nextPatchSetId(notes.getChange().currentPatchSetId());
info = patchSetInfoFactory.get(receivePack.getRevWalk(), newCommit, psId);
cmd = new ReceiveCommand(ObjectId.zeroId(), newCommitId, psId.toRefName());
}
}
+ private PatchSet.Id nextPatchSetId(PatchSet.Id psId) throws IOException {
+ PatchSet.Id next = ChangeUtil.nextPatchSetId(psId);
+ while (receivePackRefCache.exactRef(next.toRefName()) != null) {
+ next = ChangeUtil.nextPatchSetId(next);
+ }
+ return next;
+ }
+
void addOps(BatchUpdate bu, @Nullable Task progress) throws IOException {
try (TraceTimer traceTimer = newTimer("addOps")) {
if (magicBranch != null && magicBranch.edit) {
@@ -3091,45 +3126,6 @@
}
}
- private List<Ref> refs(Change.Id changeId) {
- return refsByChange().get(changeId);
- }
-
- private void initChangeRefMaps() {
- if (refsByChange != null) {
- return;
- }
-
- try (TraceTimer traceTimer = newTimer("initChangeRefMaps")) {
- int estRefsPerChange = 4;
- refsById = MultimapBuilder.hashKeys().arrayListValues().build();
- refsByChange =
- MultimapBuilder.hashKeys(allRefs().size() / estRefsPerChange)
- .arrayListValues(estRefsPerChange)
- .build();
- for (Ref ref : allRefs().values()) {
- ObjectId obj = ref.getObjectId();
- if (obj != null) {
- PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
- if (psId != null) {
- refsById.put(obj, ref);
- refsByChange.put(psId.changeId(), ref);
- }
- }
- }
- }
- }
-
- private ListMultimap<Change.Id, Ref> refsByChange() {
- initChangeRefMaps();
- return refsByChange;
- }
-
- private ListMultimap<ObjectId, Ref> changeRefsById() {
- initChangeRefMaps();
- return refsById;
- }
-
private static boolean parentsEqual(RevCommit a, RevCommit b) {
if (a.getParentCount() != b.getParentCount()) {
return false;
@@ -3214,7 +3210,6 @@
if (!(parsedObject instanceof RevCommit)) {
return;
}
- ListMultimap<ObjectId, Ref> existing = changeRefsById();
walk.markStart((RevCommit) parsedObject);
markHeadsAsUninteresting(walk, cmd.getRefName());
int limit = receiveConfig.maxBatchCommits;
@@ -3231,7 +3226,7 @@
"more than %d commits, and %s not set", limit, PUSH_OPTION_SKIP_VALIDATION));
return;
}
- if (existing.keySet().contains(c)) {
+ if (!receivePackRefCache.tipsFromObjectId(c, RefNames.REFS_CHANGES).isEmpty()) {
continue;
}
@@ -3281,7 +3276,6 @@
rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
}
- ListMultimap<ObjectId, Ref> byCommit = changeRefsById();
Map<Change.Key, ChangeNotes> byKey = null;
List<ReplaceRequest> replaceAndClose = new ArrayList<>();
@@ -3291,7 +3285,8 @@
for (RevCommit c; (c = rw.next()) != null; ) {
rw.parseBody(c);
- for (Ref ref : byCommit.get(c.copy())) {
+ for (Ref ref :
+ receivePackRefCache.tipsFromObjectId(c.copy(), RefNames.REFS_CHANGES)) {
PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
Optional<ChangeNotes> notes = getChangeNotes(psId.changeId());
if (notes.isPresent() && notes.get().getChange().getDest().equals(branch)) {
@@ -3403,13 +3398,6 @@
}
}
- // allRefsWatcher hooks into the protocol negotation to get a list of all known refs.
- // This is used as a cache of ref -> sha1 values, and to build an inverse index
- // of (change => list of refs) and a (SHA1 => refs).
- private Map<String, Ref> allRefs() {
- return allRefsWatcher.getAllRefs();
- }
-
private TraceTimer newTimer(String name) {
return newTimer(getClass(), name);
}
diff --git a/java/com/google/gerrit/server/git/receive/ReceivePackRefCache.java b/java/com/google/gerrit/server/git/receive/ReceivePackRefCache.java
new file mode 100644
index 0000000..376ab2d
--- /dev/null
+++ b/java/com/google/gerrit/server/git/receive/ReceivePackRefCache.java
@@ -0,0 +1,174 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.receive;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.base.Supplier;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.MultimapBuilder;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.RefNames;
+import java.io.IOException;
+import java.util.Map;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+
+/**
+ * Simple cache for accessing refs by name, prefix or {@link ObjectId}. Intended to be used when
+ * processing a {@code git push}.
+ *
+ * <p>This class is not thread safe.
+ */
+public interface ReceivePackRefCache {
+
+ /**
+ * Returns an instance that delegates all calls to the provided {@link RefDatabase}. To be used in
+ * tests or when the ref database is fast with forward (name to {@link ObjectId}) and inverse
+ * ({@code ObjectId} to name) lookups.
+ */
+ static ReceivePackRefCache noCache(RefDatabase delegate) {
+ return new NoCache(delegate);
+ }
+
+ /**
+ * Returns an instance that answers calls based on refs previously advertised and captured in
+ * {@link AllRefsWatcher}. Speeds up inverse lookups by building a {@code Map<ObjectId,
+ * List<Ref>>} and a {@code Map<Change.Id, List<Ref>>}.
+ *
+ * <p>This implementation speeds up lookups when the ref database does not support inverse ({@code
+ * ObjectId} to name) lookups.
+ */
+ static ReceivePackRefCache withAdvertisedRefs(Supplier<Map<String, Ref>> allRefsSupplier) {
+ return new WithAdvertisedRefs(allRefsSupplier);
+ }
+
+ /** Returns a list of refs whose name starts with {@code prefix} that point to {@code id}. */
+ ImmutableList<Ref> tipsFromObjectId(ObjectId id, @Nullable String prefix) throws IOException;
+
+ /** Returns all refs whose name starts with {@code prefix}. */
+ ImmutableList<Ref> byPrefix(String prefix) throws IOException;
+
+ /** Returns a ref whose name matches {@code ref} or {@code null} if such a ref does not exist. */
+ @Nullable
+ Ref exactRef(String ref) throws IOException;
+
+ class NoCache implements ReceivePackRefCache {
+ private final RefDatabase delegate;
+
+ private NoCache(RefDatabase delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public ImmutableList<Ref> tipsFromObjectId(ObjectId id, @Nullable String prefix)
+ throws IOException {
+ return delegate.getTipsWithSha1(id).stream()
+ .filter(r -> prefix == null || r.getName().startsWith(prefix))
+ .collect(toImmutableList());
+ }
+
+ @Override
+ public ImmutableList<Ref> byPrefix(String prefix) throws IOException {
+ return delegate.getRefsByPrefix(prefix).stream().collect(toImmutableList());
+ }
+
+ @Override
+ @Nullable
+ public Ref exactRef(String name) throws IOException {
+ return delegate.exactRef(name);
+ }
+ }
+
+ class WithAdvertisedRefs implements ReceivePackRefCache {
+ /** We estimate that a change has an average of 4 patch sets plus the meta ref. */
+ private static final int ESTIMATED_NUMBER_OF_REFS_PER_CHANGE = 5;
+
+ private final Supplier<Map<String, Ref>> allRefsSupplier;
+
+ // Collections lazily populated during processing.
+ private Map<String, Ref> allRefs;
+ /** Contains only patch set refs. */
+ private ListMultimap<Change.Id, Ref> refsByChange;
+ /** Contains all refs. */
+ private ListMultimap<ObjectId, Ref> refsByObjectId;
+
+ private WithAdvertisedRefs(Supplier<Map<String, Ref>> allRefsSupplier) {
+ this.allRefsSupplier = allRefsSupplier;
+ }
+
+ @Override
+ public ImmutableList<Ref> tipsFromObjectId(ObjectId id, String prefix) {
+ lazilyInitRefMaps();
+ return refsByObjectId.get(id).stream()
+ .filter(r -> prefix == null || r.getName().startsWith(prefix))
+ .collect(toImmutableList());
+ }
+
+ @Override
+ public ImmutableList<Ref> byPrefix(String prefix) {
+ lazilyInitRefMaps();
+ if (RefNames.isRefsChanges(prefix)) {
+ Change.Id cId = Change.Id.fromRefPart(prefix);
+ if (cId != null) {
+ return refsByChange.get(cId).stream()
+ .filter(r -> r.getName().startsWith(prefix))
+ .collect(toImmutableList());
+ }
+ }
+ return allRefs().values().stream()
+ .filter(r -> r.getName().startsWith(prefix))
+ .collect(toImmutableList());
+ }
+
+ @Override
+ @Nullable
+ public Ref exactRef(String name) {
+ return allRefs().get(name);
+ }
+
+ private Map<String, Ref> allRefs() {
+ if (allRefs == null) {
+ allRefs = allRefsSupplier.get();
+ }
+ return allRefs;
+ }
+
+ private void lazilyInitRefMaps() {
+ if (refsByChange != null) {
+ return;
+ }
+
+ refsByObjectId = MultimapBuilder.hashKeys().arrayListValues().build();
+ refsByChange =
+ MultimapBuilder.hashKeys(allRefs().size() / ESTIMATED_NUMBER_OF_REFS_PER_CHANGE)
+ .arrayListValues(ESTIMATED_NUMBER_OF_REFS_PER_CHANGE)
+ .build();
+ for (Ref ref : allRefs().values()) {
+ ObjectId objectId = ref.getObjectId();
+ if (objectId != null) {
+ refsByObjectId.put(objectId, ref);
+ Change.Id changeId = Change.Id.fromRef(ref.getName());
+ if (changeId != null) {
+ refsByChange.put(changeId, ref);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/project/LabelDefinitionJson.java b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
new file mode 100644
index 0000000..2ecd8c2
--- /dev/null
+++ b/java/com/google/gerrit/server/project/LabelDefinitionJson.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static java.util.stream.Collectors.toMap;
+
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+
+public class LabelDefinitionJson {
+ public static LabelDefinitionInfo format(Project.NameKey projectName, LabelType labelType) {
+ LabelDefinitionInfo label = new LabelDefinitionInfo();
+ label.name = labelType.getName();
+ label.projectName = projectName.get();
+ label.function = labelType.getFunction().getFunctionName();
+ label.values =
+ labelType.getValues().stream().collect(toMap(LabelValue::formatValue, LabelValue::getText));
+ label.defaultValue = labelType.getDefaultValue();
+ label.branches = labelType.getRefPatterns() != null ? labelType.getRefPatterns() : null;
+ label.canOverride = toBoolean(labelType.canOverride());
+ label.copyAnyScore = toBoolean(labelType.isCopyAnyScore());
+ label.copyMinScore = toBoolean(labelType.isCopyMinScore());
+ label.copyMaxScore = toBoolean(labelType.isCopyMaxScore());
+ label.copyAllScoresIfNoChange = toBoolean(labelType.isCopyAllScoresIfNoChange());
+ label.copyAllScoresIfNoCodeChange = toBoolean(labelType.isCopyAllScoresIfNoCodeChange());
+ label.copyAllScoresOnTrivialRebase = toBoolean(labelType.isCopyAllScoresOnTrivialRebase());
+ label.copyAllScoresOnMergeFirstParentUpdate =
+ toBoolean(labelType.isCopyAllScoresOnMergeFirstParentUpdate());
+ label.allowPostSubmit = toBoolean(labelType.allowPostSubmit());
+ label.ignoreSelfApproval = toBoolean(labelType.ignoreSelfApproval());
+ return label;
+ }
+
+ private static Boolean toBoolean(boolean v) {
+ return v ? v : null;
+ }
+
+ private LabelDefinitionJson() {}
+}
diff --git a/java/com/google/gerrit/server/project/LabelResource.java b/java/com/google/gerrit/server/project/LabelResource.java
new file mode 100644
index 0000000..a7a2f07
--- /dev/null
+++ b/java/com/google/gerrit/server/project/LabelResource.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class LabelResource implements RestResource {
+ public static final TypeLiteral<RestView<LabelResource>> LABEL_KIND =
+ new TypeLiteral<RestView<LabelResource>>() {};
+
+ private final ProjectResource project;
+ private final LabelType labelType;
+
+ public LabelResource(ProjectResource project, LabelType labelType) {
+ this.project = project;
+ this.labelType = labelType;
+ }
+
+ public ProjectResource getProject() {
+ return project;
+ }
+
+ public LabelType getLabelType() {
+ return labelType;
+ }
+}
diff --git a/java/com/google/gerrit/server/project/ProjectConfig.java b/java/com/google/gerrit/server/project/ProjectConfig.java
index 44d9d98..4d551a2 100644
--- a/java/com/google/gerrit/server/project/ProjectConfig.java
+++ b/java/com/google/gerrit/server/project/ProjectConfig.java
@@ -19,6 +19,7 @@
import static com.google.gerrit.common.data.Permission.isPermission;
import static com.google.gerrit.entities.Project.DEFAULT_SUBMIT_TYPE;
import static com.google.gerrit.server.permissions.PluginPermissionsUtil.isValidPluginPermission;
+import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import com.google.common.base.CharMatcher;
@@ -112,10 +113,10 @@
public static final String KEY_CAN_OVERRIDE = "canOverride";
public static final String KEY_BRANCH = "branch";
- private static final String KEY_MATCH = "match";
+ public static final String KEY_MATCH = "match";
private static final String KEY_HTML = "html";
- private static final String KEY_LINK = "link";
- private static final String KEY_ENABLED = "enabled";
+ public static final String KEY_LINK = "link";
+ public static final String KEY_ENABLED = "enabled";
public static final String PROJECT_CONFIG = "project.config";
@@ -291,6 +292,11 @@
commentLinkSections.put(commentLink.name, commentLink);
}
+ public void removeCommentLinkSection(String name) {
+ requireNonNull(name);
+ requireNonNull(commentLinkSections.remove(name));
+ }
+
private ProjectConfig(Project.NameKey projectName, @Nullable StoredConfig baseConfig) {
this.projectName = projectName;
this.baseConfig = baseConfig;
@@ -1488,6 +1494,8 @@
List<String> refPatterns = label.getRefPatterns();
if (refPatterns != null && !refPatterns.isEmpty()) {
rc.setStringList(LABEL, name, KEY_BRANCH, refPatterns);
+ } else {
+ rc.unset(LABEL, name, KEY_BRANCH);
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
index 30cfad6..63f5bbe 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
@@ -28,6 +28,7 @@
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountLoader;
@@ -146,21 +147,18 @@
@Singleton
public static class DefaultDeleteChangeMessage
- extends RetryingRestModifyView<ChangeMessageResource, Input, ChangeMessageInfo> {
+ implements RestModifyView<ChangeMessageResource, Input> {
private final DeleteChangeMessage deleteChangeMessage;
@Inject
- public DefaultDeleteChangeMessage(
- DeleteChangeMessage deleteChangeMessage, RetryHelper retryHelper) {
- super(retryHelper);
+ public DefaultDeleteChangeMessage(DeleteChangeMessage deleteChangeMessage) {
this.deleteChangeMessage = deleteChangeMessage;
}
@Override
- protected Response<ChangeMessageInfo> applyImpl(
- BatchUpdate.Factory updateFactory, ChangeMessageResource resource, Input input)
- throws Exception {
- return deleteChangeMessage.applyImpl(updateFactory, resource, new DeleteChangeMessageInput());
+ public Response<ChangeMessageInfo> apply(ChangeMessageResource resource, Input input)
+ throws RestApiException {
+ return deleteChangeMessage.apply(resource, new DeleteChangeMessageInput());
}
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/Module.java b/java/com/google/gerrit/server/restapi/change/Module.java
index a57bd64..539463f 100644
--- a/java/com/google/gerrit/server/restapi/change/Module.java
+++ b/java/com/google/gerrit/server/restapi/change/Module.java
@@ -96,6 +96,7 @@
post(CHANGE_KIND, "hashtags").to(PostHashtags.class);
post(CHANGE_KIND, "restore").to(Restore.class);
post(CHANGE_KIND, "revert").to(Revert.class);
+ post(CHANGE_KIND, "revert_submission").to(RevertSubmission.class);
post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
get(CHANGE_KIND, "submitted_together").to(SubmittedTogether.class);
post(CHANGE_KIND, "rebase").to(Rebase.CurrentRevision.class);
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index af8f971..50d1ad0 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -260,28 +260,23 @@
return description;
}
- public static class CurrentRevision
- extends RetryingRestModifyView<ChangeResource, RebaseInput, ChangeInfo> {
+ public static class CurrentRevision implements RestModifyView<ChangeResource, RebaseInput> {
private final PatchSetUtil psUtil;
private final Rebase rebase;
@Inject
- CurrentRevision(RetryHelper retryHelper, PatchSetUtil psUtil, Rebase rebase) {
- super(retryHelper);
+ CurrentRevision(PatchSetUtil psUtil, Rebase rebase) {
this.psUtil = psUtil;
this.rebase = rebase;
}
@Override
- protected Response<ChangeInfo> applyImpl(
- BatchUpdate.Factory updateFactory, ChangeResource rsrc, RebaseInput input)
- throws Exception {
+ public Response<ChangeInfo> apply(ChangeResource rsrc, RebaseInput input) throws Exception {
PatchSet ps = psUtil.current(rsrc.getNotes());
if (ps == null) {
throw new ResourceConflictException("current revision is missing");
}
- return Response.ok(
- rebase.applyImpl(updateFactory, new RevisionResource(rsrc, ps), input).value());
+ return Response.ok(rebase.apply(new RevisionResource(rsrc, ps), input).value());
}
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
new file mode 100644
index 0000000..105ffa2
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -0,0 +1,166 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.change;
+
+import static com.google.gerrit.extensions.conditions.BooleanCondition.and;
+import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.extensions.api.changes.RevertInput;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.RevertSubmissionInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.webui.UiAction;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.project.ContributorAgreementsChecker;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.RetryHelper;
+import com.google.gerrit.server.update.RetryingRestModifyView;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang.RandomStringUtils;
+
+@Singleton
+public class RevertSubmission
+ extends RetryingRestModifyView<ChangeResource, RevertInput, RevertSubmissionInfo>
+ implements UiAction<ChangeResource> {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final Revert revert;
+ private final Provider<InternalChangeQuery> queryProvider;
+ private final ChangeResource.Factory changeResourceFactory;
+ private final Provider<CurrentUser> user;
+ private final PermissionBackend permissionBackend;
+ private final ProjectCache projectCache;
+ private final PatchSetUtil psUtil;
+ private final ContributorAgreementsChecker contributorAgreements;
+
+ @Inject
+ RevertSubmission(
+ RetryHelper retryHelper,
+ Revert revert,
+ Provider<InternalChangeQuery> queryProvider,
+ ChangeResource.Factory changeResourceFactory,
+ Provider<CurrentUser> user,
+ PermissionBackend permissionBackend,
+ ProjectCache projectCache,
+ PatchSetUtil psUtil,
+ ContributorAgreementsChecker contributorAgreements) {
+ super(retryHelper);
+ this.revert = revert;
+ this.queryProvider = queryProvider;
+ this.changeResourceFactory = changeResourceFactory;
+ this.user = user;
+ this.permissionBackend = permissionBackend;
+ this.projectCache = projectCache;
+ this.psUtil = psUtil;
+ this.contributorAgreements = contributorAgreements;
+ }
+
+ @Override
+ public Response<RevertSubmissionInfo> applyImpl(
+ BatchUpdate.Factory updateFactory, ChangeResource changeResource, RevertInput input)
+ throws Exception {
+
+ if (!changeResource.getChange().isMerged()) {
+ throw new ResourceConflictException(
+ String.format("change is %s.", ChangeUtil.status(changeResource.getChange())));
+ }
+
+ String submissionId =
+ requireNonNull(
+ changeResource.getChange().getSubmissionId(),
+ String.format("merged change %s has no submission ID", changeResource.getId()));
+
+ List<ChangeData> changeDatas = queryProvider.get().bySubmissionId(submissionId);
+
+ for (ChangeData changeData : changeDatas) {
+ Change change = changeData.change();
+
+ // Might do the permission tests multiple times, but these are necessary to ensure that the
+ // user has permissions to revert all changes. If they lack any permission, no revert will be
+ // done.
+
+ contributorAgreements.check(change.getProject(), changeResource.getUser());
+ permissionBackend.currentUser().ref(change.getDest()).check(CREATE_CHANGE);
+ permissionBackend.currentUser().change(changeData).check(ChangePermission.READ);
+ projectCache.checkedGet(change.getProject()).checkStatePermitsWrite();
+
+ requireNonNull(
+ psUtil.get(changeData.notes(), change.currentPatchSetId()),
+ String.format(
+ "current patch set %s of change %s not found",
+ change.currentPatchSetId(), change.currentPatchSetId()));
+ }
+ return Response.ok(revertSubmission(changeDatas, input, submissionId));
+ }
+
+ private RevertSubmissionInfo revertSubmission(
+ List<ChangeData> changeDatas, RevertInput input, String submissionId) throws Exception {
+ List<ChangeInfo> results;
+ results = new ArrayList<>();
+ if (input.topic == null) {
+ input.topic =
+ String.format(
+ "revert-%s-%s", submissionId, RandomStringUtils.randomAlphabetic(10).toUpperCase());
+ }
+ for (ChangeData changeData : changeDatas) {
+ ChangeResource change = changeResourceFactory.create(changeData.notes(), user.get());
+ // Reverts are done with retrying by using RetryingRestModifyView.
+ results.add(revert.apply(change, input).value());
+ }
+ RevertSubmissionInfo revertSubmissionInfo = new RevertSubmissionInfo();
+ revertSubmissionInfo.revertChanges = results;
+ return revertSubmissionInfo;
+ }
+
+ @Override
+ public Description getDescription(ChangeResource rsrc) {
+ Change change = rsrc.getChange();
+ boolean projectStatePermitsWrite = false;
+ try {
+ projectStatePermitsWrite = projectCache.checkedGet(rsrc.getProject()).statePermitsWrite();
+ } catch (IOException e) {
+ logger.atSevere().withCause(e).log(
+ "Failed to check if project state permits write: %s", rsrc.getProject());
+ }
+ return new UiAction.Description()
+ .setLabel("Revert submission")
+ .setTitle(
+ "Revert this change and all changes that have been submitted together with this change")
+ .setVisible(
+ and(
+ change.isMerged() && projectStatePermitsWrite,
+ permissionBackend
+ .user(rsrc.getUser())
+ .ref(change.getDest())
+ .testCond(CREATE_CHANGE)));
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/CreateLabel.java b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
new file mode 100644
index 0000000..03b9452
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/CreateLabel.java
@@ -0,0 +1,188 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.LabelDefinitionJson;
+import com.google.gerrit.server.project.LabelResource;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class CreateLabel
+ implements RestCollectionCreateView<ProjectResource, LabelResource, LabelDefinitionInput> {
+ private final Provider<CurrentUser> user;
+ private final PermissionBackend permissionBackend;
+ private final MetaDataUpdate.User updateFactory;
+ private final ProjectConfig.Factory projectConfigFactory;
+ private final ProjectCache projectCache;
+
+ @Inject
+ public CreateLabel(
+ Provider<CurrentUser> user,
+ PermissionBackend permissionBackend,
+ MetaDataUpdate.User updateFactory,
+ ProjectConfig.Factory projectConfigFactory,
+ ProjectCache projectCache) {
+ this.user = user;
+ this.permissionBackend = permissionBackend;
+ this.updateFactory = updateFactory;
+ this.projectConfigFactory = projectConfigFactory;
+ this.projectCache = projectCache;
+ }
+
+ @Override
+ public Response<LabelDefinitionInfo> apply(
+ ProjectResource rsrc, IdString id, LabelDefinitionInput input)
+ throws AuthException, BadRequestException, ResourceConflictException,
+ PermissionBackendException, IOException, ConfigInvalidException {
+ if (!user.get().isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+
+ permissionBackend
+ .currentUser()
+ .project(rsrc.getNameKey())
+ .check(ProjectPermission.WRITE_CONFIG);
+
+ if (input == null) {
+ input = new LabelDefinitionInput();
+ }
+
+ if (input.name != null && !input.name.equals(id.get())) {
+ throw new BadRequestException("name in input must match name in URL");
+ }
+
+ try (MetaDataUpdate md = updateFactory.create(rsrc.getNameKey())) {
+ ProjectConfig config = projectConfigFactory.read(md);
+
+ if (config.getLabelSections().containsKey(id.get())) {
+ throw new ResourceConflictException(String.format("label %s already exists", id.get()));
+ }
+
+ for (String labelName : config.getLabelSections().keySet()) {
+ if (labelName.equalsIgnoreCase(id.get())) {
+ throw new ResourceConflictException(
+ String.format("label %s conflicts with existing label %s", id.get(), labelName));
+ }
+ }
+
+ if (input.values == null || input.values.isEmpty()) {
+ throw new BadRequestException("values are required");
+ }
+
+ List<LabelValue> values = LabelDefinitionInputParser.parseValues(input.values);
+
+ LabelType labelType;
+ try {
+ labelType = new LabelType(id.get(), values);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException("invalid name: " + id.get(), e);
+ }
+
+ if (input.function != null && !input.function.trim().isEmpty()) {
+ labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function));
+ } else {
+ labelType.setFunction(LabelFunction.MAX_WITH_BLOCK);
+ }
+
+ if (input.defaultValue != null) {
+ labelType.setDefaultValue(
+ LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue));
+ }
+
+ if (input.branches != null) {
+ labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches));
+ }
+
+ if (input.canOverride != null) {
+ labelType.setCanOverride(input.canOverride);
+ }
+
+ if (input.copyAnyScore != null) {
+ labelType.setCopyAnyScore(input.copyAnyScore);
+ }
+
+ if (input.copyMinScore != null) {
+ labelType.setCopyMinScore(input.copyMinScore);
+ }
+
+ if (input.copyMaxScore != null) {
+ labelType.setCopyMaxScore(input.copyMaxScore);
+ }
+
+ if (input.copyAllScoresIfNoChange != null) {
+ labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange);
+ }
+
+ if (input.copyAllScoresIfNoCodeChange != null) {
+ labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange);
+ }
+
+ if (input.copyAllScoresOnTrivialRebase != null) {
+ labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase);
+ }
+
+ if (input.copyAllScoresOnMergeFirstParentUpdate != null) {
+ labelType.setCopyAllScoresOnMergeFirstParentUpdate(
+ input.copyAllScoresOnMergeFirstParentUpdate);
+ }
+
+ if (input.allowPostSubmit != null) {
+ labelType.setAllowPostSubmit(input.allowPostSubmit);
+ }
+
+ if (input.ignoreSelfApproval != null) {
+ labelType.setIgnoreSelfApproval(input.ignoreSelfApproval);
+ }
+
+ if (input.commitMessage != null) {
+ md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
+ } else {
+ md.setMessage("Update label");
+ }
+
+ config.getLabelSections().put(labelType.getName(), labelType);
+ config.commit(md);
+
+ projectCache.evict(rsrc.getProjectState().getProject());
+
+ return Response.created(LabelDefinitionJson.format(rsrc.getNameKey(), labelType));
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/DeleteLabel.java b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
new file mode 100644
index 0000000..5464abf
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/DeleteLabel.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.common.InputWithCommitMessage;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.LabelResource;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class DeleteLabel implements RestModifyView<LabelResource, InputWithCommitMessage> {
+ private final Provider<CurrentUser> user;
+ private final PermissionBackend permissionBackend;
+ private final MetaDataUpdate.User updateFactory;
+ private final ProjectConfig.Factory projectConfigFactory;
+ private final ProjectCache projectCache;
+
+ @Inject
+ public DeleteLabel(
+ Provider<CurrentUser> user,
+ PermissionBackend permissionBackend,
+ MetaDataUpdate.User updateFactory,
+ ProjectConfig.Factory projectConfigFactory,
+ ProjectCache projectCache) {
+ this.user = user;
+ this.permissionBackend = permissionBackend;
+ this.updateFactory = updateFactory;
+ this.projectConfigFactory = projectConfigFactory;
+ this.projectCache = projectCache;
+ }
+
+ @Override
+ public Response<?> apply(LabelResource rsrc, InputWithCommitMessage input)
+ throws AuthException, ResourceNotFoundException, PermissionBackendException, IOException,
+ ConfigInvalidException {
+ if (!user.get().isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+
+ permissionBackend
+ .currentUser()
+ .project(rsrc.getProject().getNameKey())
+ .check(ProjectPermission.WRITE_CONFIG);
+
+ if (input == null) {
+ input = new InputWithCommitMessage();
+ }
+
+ try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) {
+ ProjectConfig config = projectConfigFactory.read(md);
+
+ if (!config.getLabelSections().containsKey(rsrc.getLabelType().getName())) {
+ throw new ResourceNotFoundException(IdString.fromDecoded(rsrc.getLabelType().getName()));
+ }
+
+ config.getLabelSections().remove(rsrc.getLabelType().getName());
+
+ if (input.commitMessage != null) {
+ md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
+ } else {
+ md.setMessage("Delete label");
+ }
+
+ config.commit(md);
+ }
+
+ projectCache.evict(rsrc.getProject().getProjectState().getProject());
+
+ return Response.none();
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/GetLabel.java b/java/com/google/gerrit/server/restapi/project/GetLabel.java
new file mode 100644
index 0000000..626cb42
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/GetLabel.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.project.LabelDefinitionJson;
+import com.google.gerrit.server.project.LabelResource;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GetLabel implements RestReadView<LabelResource> {
+ @Override
+ public Response<LabelDefinitionInfo> apply(LabelResource rsrc)
+ throws AuthException, BadRequestException {
+ return Response.ok(
+ LabelDefinitionJson.format(rsrc.getProject().getNameKey(), rsrc.getLabelType()));
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
new file mode 100644
index 0000000..a45c67f
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/LabelDefinitionInputParser.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.common.primitives.Shorts;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.exceptions.InvalidNameException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.server.project.RefPattern;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+
+public class LabelDefinitionInputParser {
+ public static LabelFunction parseFunction(String functionString) throws BadRequestException {
+ Optional<LabelFunction> function = LabelFunction.parse(functionString.trim());
+ return function.orElseThrow(
+ () -> new BadRequestException("unknown function: " + functionString));
+ }
+
+ public static List<LabelValue> parseValues(Map<String, String> values)
+ throws BadRequestException {
+ List<LabelValue> valueList = new ArrayList<>();
+ for (Entry<String, String> e : values.entrySet()) {
+ short value;
+ try {
+ value = Shorts.checkedCast(PermissionRule.parseInt(e.getKey().trim()));
+ } catch (NumberFormatException ex) {
+ throw new BadRequestException("invalid value: " + e.getKey(), ex);
+ }
+ String valueDescription = e.getValue().trim();
+ if (valueDescription.isEmpty()) {
+ throw new BadRequestException("description for value '" + e.getKey() + "' cannot be empty");
+ }
+ valueList.add(new LabelValue(value, valueDescription));
+ }
+ return valueList;
+ }
+
+ public static short parseDefaultValue(LabelType labelType, short defaultValue)
+ throws BadRequestException {
+ if (labelType.getValue(defaultValue) == null) {
+ throw new BadRequestException("invalid default value: " + defaultValue);
+ }
+ return defaultValue;
+ }
+
+ public static List<String> parseBranches(List<String> branches) throws BadRequestException {
+ List<String> validBranches = new ArrayList<>();
+ for (String branch : branches) {
+ String newBranch = branch.trim();
+ if (newBranch.isEmpty()) {
+ continue;
+ }
+ if (!RefPattern.isRE(newBranch) && !newBranch.startsWith(RefNames.REFS)) {
+ newBranch = RefNames.REFS_HEADS + newBranch;
+ }
+ try {
+ RefPattern.validate(newBranch);
+ } catch (InvalidNameException e) {
+ throw new BadRequestException("invalid branch: " + branch, e);
+ }
+ validBranches.add(newBranch);
+ }
+ return validBranches;
+ }
+
+ private LabelDefinitionInputParser() {}
+}
diff --git a/java/com/google/gerrit/server/restapi/project/LabelsCollection.java b/java/com/google/gerrit/server/restapi/project/LabelsCollection.java
new file mode 100644
index 0000000..0409729
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/LabelsCollection.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.LabelResource;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class LabelsCollection implements ChildCollection<ProjectResource, LabelResource> {
+ private final Provider<ListLabels> list;
+ private final DynamicMap<RestView<LabelResource>> views;
+ private final Provider<CurrentUser> user;
+ private final PermissionBackend permissionBackend;
+
+ @Inject
+ LabelsCollection(
+ Provider<ListLabels> list,
+ DynamicMap<RestView<LabelResource>> views,
+ Provider<CurrentUser> user,
+ PermissionBackend permissionBackend) {
+ this.list = list;
+ this.views = views;
+ this.user = user;
+ this.permissionBackend = permissionBackend;
+ }
+
+ @Override
+ public RestView<ProjectResource> list() throws RestApiException {
+ return list.get();
+ }
+
+ @Override
+ public LabelResource parse(ProjectResource parent, IdString id)
+ throws AuthException, ResourceNotFoundException, PermissionBackendException {
+ if (!user.get().isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+
+ permissionBackend
+ .currentUser()
+ .project(parent.getNameKey())
+ .check(ProjectPermission.READ_CONFIG);
+ LabelType labelType = parent.getProjectState().getConfig().getLabelSections().get(id.get());
+ if (labelType == null) {
+ throw new ResourceNotFoundException(id);
+ }
+ return new LabelResource(parent, labelType);
+ }
+
+ @Override
+ public DynamicMap<RestView<LabelResource>> views() {
+ return views;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/ListLabels.java b/java/com/google/gerrit/server/restapi/project/ListLabels.java
new file mode 100644
index 0000000..19a8915
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/ListLabels.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.LabelDefinitionJson;
+import com.google.gerrit.server.project.ProjectResource;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import org.kohsuke.args4j.Option;
+
+public class ListLabels implements RestReadView<ProjectResource> {
+ private final Provider<CurrentUser> user;
+ private final PermissionBackend permissionBackend;
+
+ @Inject
+ public ListLabels(Provider<CurrentUser> user, PermissionBackend permissionBackend) {
+ this.user = user;
+ this.permissionBackend = permissionBackend;
+ }
+
+ @Option(name = "--inherited", usage = "to include inherited label definitions")
+ private boolean inherited;
+
+ public ListLabels withInherited(boolean inherited) {
+ this.inherited = inherited;
+ return this;
+ }
+
+ @Override
+ public Response<List<LabelDefinitionInfo>> apply(ProjectResource rsrc)
+ throws AuthException, PermissionBackendException {
+ if (!user.get().isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+
+ if (inherited) {
+ List<LabelDefinitionInfo> allLabels = new ArrayList<>();
+ for (ProjectState projectState : rsrc.getProjectState().treeInOrder()) {
+ try {
+ permissionBackend
+ .currentUser()
+ .project(projectState.getNameKey())
+ .check(ProjectPermission.READ_CONFIG);
+ } catch (AuthException e) {
+ throw new AuthException(projectState.getNameKey() + ": " + e.getMessage(), e);
+ }
+ allLabels.addAll(listLabels(projectState));
+ }
+ return Response.ok(allLabels);
+ }
+
+ permissionBackend.currentUser().project(rsrc.getNameKey()).check(ProjectPermission.READ_CONFIG);
+ return Response.ok(listLabels(rsrc.getProjectState()));
+ }
+
+ private List<LabelDefinitionInfo> listLabels(ProjectState projectState) {
+ Collection<LabelType> labelTypes = projectState.getConfig().getLabelSections().values();
+ List<LabelDefinitionInfo> labels = new ArrayList<>(labelTypes.size());
+ for (LabelType labelType : labelTypes) {
+ labels.add(LabelDefinitionJson.format(projectState.getNameKey(), labelType));
+ }
+ labels.sort(Comparator.comparing(l -> l.name));
+ return labels;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index 065facd..4ed21cc 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -19,6 +19,7 @@
import static com.google.gerrit.server.project.CommitResource.COMMIT_KIND;
import static com.google.gerrit.server.project.DashboardResource.DASHBOARD_KIND;
import static com.google.gerrit.server.project.FileResource.FILE_KIND;
+import static com.google.gerrit.server.project.LabelResource.LABEL_KIND;
import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
import static com.google.gerrit.server.project.TagResource.TAG_KIND;
@@ -42,6 +43,7 @@
DynamicMap.mapOf(binder(), FILE_KIND);
DynamicMap.mapOf(binder(), COMMIT_KIND);
DynamicMap.mapOf(binder(), TAG_KIND);
+ DynamicMap.mapOf(binder(), LABEL_KIND);
DynamicSet.bind(binder(), GerritConfigListener.class).to(SetParent.class);
@@ -65,6 +67,12 @@
child(PROJECT_KIND, "children").to(ChildProjectsCollection.class);
get(CHILD_PROJECT_KIND).to(GetChildProject.class);
+ child(PROJECT_KIND, "labels").to(LabelsCollection.class);
+ create(LABEL_KIND).to(CreateLabel.class);
+ get(LABEL_KIND).to(GetLabel.class);
+ put(LABEL_KIND).to(SetLabel.class);
+ delete(LABEL_KIND).to(DeleteLabel.class);
+
get(PROJECT_KIND, "HEAD").to(GetHead.class);
put(PROJECT_KIND, "HEAD").to(SetHead.class);
diff --git a/java/com/google/gerrit/server/restapi/project/PutConfig.java b/java/com/google/gerrit/server/restapi/project/PutConfig.java
index 696ac37..a0badd7 100644
--- a/java/com/google/gerrit/server/restapi/project/PutConfig.java
+++ b/java/com/google/gerrit/server/restapi/project/PutConfig.java
@@ -14,11 +14,17 @@
package com.google.gerrit.server.restapi.project;
+import static com.google.gerrit.server.project.ProjectConfig.COMMENTLINK;
+import static com.google.gerrit.server.project.ProjectConfig.KEY_ENABLED;
+import static com.google.gerrit.server.project.ProjectConfig.KEY_LINK;
+import static com.google.gerrit.server.project.ProjectConfig.KEY_MATCH;
+
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.api.projects.CommentLinkInput;
import com.google.gerrit.extensions.api.projects.ConfigInfo;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.api.projects.ConfigValue;
@@ -59,6 +65,7 @@
import java.util.regex.Pattern;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
@Singleton
public class PutConfig implements RestModifyView<ProjectResource, ConfigInput> {
@@ -154,6 +161,10 @@
setPluginConfigValues(projectState, projectConfig, input.pluginConfigValues);
}
+ if (input.commentLinks != null) {
+ updateCommentLinks(projectConfig, input.commentLinks);
+ }
+
md.setMessage("Modified project settings\n");
try {
projectConfig.commit(md);
@@ -278,6 +289,25 @@
}
}
+ private void updateCommentLinks(
+ ProjectConfig projectConfig, Map<String, CommentLinkInput> input) {
+ for (Map.Entry<String, CommentLinkInput> e : input.entrySet()) {
+ String name = e.getKey();
+ CommentLinkInput value = e.getValue();
+ if (value != null) {
+ // Add or update the commentlink section
+ Config cfg = new Config();
+ cfg.setString(COMMENTLINK, name, KEY_MATCH, value.match);
+ cfg.setString(COMMENTLINK, name, KEY_LINK, value.link);
+ cfg.setBoolean(COMMENTLINK, name, KEY_ENABLED, value.enabled == null || value.enabled);
+ projectConfig.addCommentLinkSection(ProjectConfig.buildCommentLink(cfg, name, false));
+ } else {
+ // Delete the commentlink section
+ projectConfig.removeCommentLinkSection(name);
+ }
+ }
+ }
+
private static void validateProjectConfigEntryIsEditable(
ProjectConfigEntry projectConfigEntry,
ProjectState projectState,
diff --git a/java/com/google/gerrit/server/restapi/project/SetLabel.java b/java/com/google/gerrit/server/restapi/project/SetLabel.java
new file mode 100644
index 0000000..b7cffce
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/SetLabel.java
@@ -0,0 +1,206 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.meta.MetaDataUpdate;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.project.LabelDefinitionJson;
+import com.google.gerrit.server.project.LabelResource;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class SetLabel implements RestModifyView<LabelResource, LabelDefinitionInput> {
+ private final Provider<CurrentUser> user;
+ private final PermissionBackend permissionBackend;
+ private final MetaDataUpdate.User updateFactory;
+ private final ProjectConfig.Factory projectConfigFactory;
+ private final ProjectCache projectCache;
+
+ @Inject
+ public SetLabel(
+ Provider<CurrentUser> user,
+ PermissionBackend permissionBackend,
+ MetaDataUpdate.User updateFactory,
+ ProjectConfig.Factory projectConfigFactory,
+ ProjectCache projectCache) {
+ this.user = user;
+ this.permissionBackend = permissionBackend;
+ this.updateFactory = updateFactory;
+ this.projectConfigFactory = projectConfigFactory;
+ this.projectCache = projectCache;
+ }
+
+ @Override
+ public Response<LabelDefinitionInfo> apply(LabelResource rsrc, LabelDefinitionInput input)
+ throws AuthException, BadRequestException, ResourceConflictException,
+ PermissionBackendException, IOException, ConfigInvalidException {
+ if (!user.get().isIdentifiedUser()) {
+ throw new AuthException("Authentication required");
+ }
+
+ permissionBackend
+ .currentUser()
+ .project(rsrc.getProject().getNameKey())
+ .check(ProjectPermission.WRITE_CONFIG);
+
+ if (input == null) {
+ input = new LabelDefinitionInput();
+ }
+
+ LabelType labelType = rsrc.getLabelType();
+
+ try (MetaDataUpdate md = updateFactory.create(rsrc.getProject().getNameKey())) {
+ boolean dirty = false;
+
+ ProjectConfig config = projectConfigFactory.read(md);
+ config.getLabelSections().remove(labelType.getName());
+
+ if (input.name != null) {
+ String newName = input.name.trim();
+ if (newName.isEmpty()) {
+ throw new BadRequestException("name cannot be empty");
+ }
+ if (!newName.equals(labelType.getName())) {
+ if (config.getLabelSections().containsKey(newName)) {
+ throw new ResourceConflictException(String.format("name %s already in use", newName));
+ }
+
+ for (String labelName : config.getLabelSections().keySet()) {
+ if (labelName.equalsIgnoreCase(newName)) {
+ throw new ResourceConflictException(
+ String.format("name %s conflicts with existing label %s", newName, labelName));
+ }
+ }
+
+ try {
+ labelType.setName(newName);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException("invalid name: " + input.name, e);
+ }
+ dirty = true;
+ }
+ }
+
+ if (input.function != null) {
+ if (input.function.trim().isEmpty()) {
+ throw new BadRequestException("function cannot be empty");
+ }
+ labelType.setFunction(LabelDefinitionInputParser.parseFunction(input.function));
+ dirty = true;
+ }
+
+ if (input.values != null) {
+ if (input.values.isEmpty()) {
+ throw new BadRequestException("values cannot be empty");
+ }
+ labelType.setValues(LabelDefinitionInputParser.parseValues(input.values));
+ dirty = true;
+ }
+
+ if (input.defaultValue != null) {
+ labelType.setDefaultValue(
+ LabelDefinitionInputParser.parseDefaultValue(labelType, input.defaultValue));
+ dirty = true;
+ }
+
+ if (input.branches != null) {
+ labelType.setRefPatterns(LabelDefinitionInputParser.parseBranches(input.branches));
+ dirty = true;
+ }
+
+ if (input.canOverride != null) {
+ labelType.setCanOverride(input.canOverride);
+ dirty = true;
+ }
+
+ if (input.copyAnyScore != null) {
+ labelType.setCopyAnyScore(input.copyAnyScore);
+ dirty = true;
+ }
+
+ if (input.copyMinScore != null) {
+ labelType.setCopyMinScore(input.copyMinScore);
+ dirty = true;
+ }
+
+ if (input.copyMaxScore != null) {
+ labelType.setCopyMaxScore(input.copyMaxScore);
+ dirty = true;
+ }
+
+ if (input.copyAllScoresIfNoChange != null) {
+ labelType.setCopyAllScoresIfNoChange(input.copyAllScoresIfNoChange);
+ }
+
+ if (input.copyAllScoresIfNoCodeChange != null) {
+ labelType.setCopyAllScoresIfNoCodeChange(input.copyAllScoresIfNoCodeChange);
+ dirty = true;
+ }
+
+ if (input.copyAllScoresOnTrivialRebase != null) {
+ labelType.setCopyAllScoresOnTrivialRebase(input.copyAllScoresOnTrivialRebase);
+ dirty = true;
+ }
+
+ if (input.copyAllScoresOnMergeFirstParentUpdate != null) {
+ labelType.setCopyAllScoresOnMergeFirstParentUpdate(
+ input.copyAllScoresOnMergeFirstParentUpdate);
+ dirty = true;
+ }
+
+ if (input.allowPostSubmit != null) {
+ labelType.setAllowPostSubmit(input.allowPostSubmit);
+ dirty = true;
+ }
+
+ if (input.ignoreSelfApproval != null) {
+ labelType.setIgnoreSelfApproval(input.ignoreSelfApproval);
+ dirty = true;
+ }
+
+ if (dirty) {
+ config.getLabelSections().put(labelType.getName(), labelType);
+
+ if (input.commitMessage != null) {
+ md.setMessage(Strings.emptyToNull(input.commitMessage.trim()));
+ } else {
+ md.setMessage("Update label");
+ }
+
+ config.commit(md);
+ projectCache.evict(rsrc.getProject().getProjectState().getProject());
+ }
+ }
+ return Response.ok(LabelDefinitionJson.format(rsrc.getProject().getNameKey(), labelType));
+ }
+}
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index a06027a..406c0a1 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -504,7 +504,7 @@
try {
integrateIntoHistory(cs);
} catch (IntegrationException e) {
- logger.atSevere().withCause(e).log("Error from integrateIntoHistory");
+ logger.atWarning().withCause(e).log("Error from integrateIntoHistory");
throw new ResourceConflictException(e.getMessage(), e);
}
return null;
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 5550d98..9059cb1 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -239,6 +239,28 @@
}
@Test
+ public void revertSubmissionWithoutCLA() throws Exception {
+ assume().that(isContributorAgreementsEnabled()).isTrue();
+
+ // Create a change succeeds when agreement is not required
+ setUseContributorAgreements(InheritableBoolean.FALSE);
+ ChangeInfo change = gApi.changes().create(newChangeInput()).get();
+
+ // Approve and submit it
+ requestScopeOperations.setApiUser(admin.id());
+ gApi.changes().id(change.changeId).current().review(ReviewInput.approve());
+ gApi.changes().id(change.changeId).current().submit(new SubmitInput());
+
+ // Revert Submission is not allowed when CLA is required but not signed
+ requestScopeOperations.setApiUser(user.id());
+ setUseContributorAgreements(InheritableBoolean.TRUE);
+ AuthException thrown =
+ assertThrows(
+ AuthException.class, () -> gApi.changes().id(change.changeId).revertSubmission());
+ assertThat(thrown).hasMessageThat().contains("Contributor Agreement");
+ }
+
+ @Test
public void revertExcludedProjectChangeWithoutCLA() throws Exception {
// Contributor agreements configured with excludeProjects = ExcludedProject
// in AbstractDaemonTest.configureContributorAgreement(...)
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index 0607a3c..0e89de2 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -21,12 +21,16 @@
import static java.util.stream.Collectors.toList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestProjectInput;
+import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.RevertInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -36,6 +40,8 @@
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.PureRevertInfo;
+import com.google.gerrit.extensions.common.RevertSubmissionInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -45,8 +51,10 @@
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Repository;
import org.junit.Test;
@@ -361,17 +369,20 @@
@Test
public void cantCreateRevertWithoutProjectWritePermission() throws Exception {
- PushOneCommit.Result r = createChange();
- gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve());
- gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+ PushOneCommit.Result result = createChange();
+ gApi.changes()
+ .id(result.getChangeId())
+ .revision(result.getCommit().name())
+ .review(ReviewInput.approve());
+ gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
projectCache.checkedGet(project).getProject().setState(ProjectState.READ_ONLY);
+ String expected = "project state " + ProjectState.READ_ONLY + " does not permit write";
ResourceConflictException thrown =
assertThrows(
- ResourceConflictException.class, () -> gApi.changes().id(r.getChangeId()).revert());
- assertThat(thrown)
- .hasMessageThat()
- .contains("project state " + ProjectState.READ_ONLY + " does not permit write");
+ ResourceConflictException.class,
+ () -> gApi.changes().id(result.getChangeId()).revert());
+ assertThat(thrown).hasMessageThat().contains(expected);
}
@Test
@@ -412,6 +423,339 @@
assertThat(thrown).hasMessageThat().contains("Not found: " + r.getChangeId());
}
+ @Test
+ @GerritConfig(name = "change.submitWholeTopic", value = "true")
+ public void cantCreateRevertSubmissionWithoutProjectWritePermission() throws Exception {
+ String secondProject = "secondProject";
+ projectOperations.newProject().name(secondProject).create();
+ TestRepository<InMemoryRepository> secondRepo =
+ cloneProject(Project.nameKey("secondProject"), admin);
+ String topic = "topic";
+ PushOneCommit.Result result1 =
+ createChange(testRepo, "master", "first change", "a.txt", "message", topic);
+ PushOneCommit.Result result2 =
+ createChange(secondRepo, "master", "second change", "b.txt", "message", topic);
+ gApi.changes().id(result1.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(result2.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(result1.getChangeId()).revision(result1.getCommit().name()).submit();
+
+ // revoke write permissions for the first repository.
+ projectCache.checkedGet(project).getProject().setState(ProjectState.READ_ONLY);
+
+ String expected = "project state " + ProjectState.READ_ONLY + " does not permit write";
+
+ // assert that if first repository has no write permissions, it will fail.
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.changes().id(result1.getChangeId()).revertSubmission());
+ assertThat(thrown).hasMessageThat().contains(expected);
+
+ // assert that if the first repository has no write permissions and a change from another
+ // repository is trying to revert the submission, it will fail.
+ thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.changes().id(result2.getChangeId()).revertSubmission());
+ assertThat(thrown).hasMessageThat().contains(expected);
+ }
+
+ @Test
+ @GerritConfig(name = "change.submitWholeTopic", value = "true")
+ public void cantCreateRevertSubmissionWithoutCreateChangePermission() throws Exception {
+ String secondProject = "secondProject";
+ projectOperations.newProject().name(secondProject).create();
+ TestRepository<InMemoryRepository> secondRepo =
+ cloneProject(Project.nameKey("secondProject"), admin);
+ String topic = "topic";
+ PushOneCommit.Result result1 =
+ createChange(testRepo, "master", "first change", "a.txt", "message", topic);
+ PushOneCommit.Result result2 =
+ createChange(secondRepo, "master", "second change", "b.txt", "message", topic);
+ gApi.changes().id(result1.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(result2.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(result1.getChangeId()).revision(result1.getCommit().name()).submit();
+
+ // revoke create change permissions for the first repository.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.PUSH).ref("refs/for/*").group(REGISTERED_USERS))
+ .update();
+
+ // assert that if first repository has no write create change, it will fail.
+ PermissionDeniedException thrown =
+ assertThrows(
+ PermissionDeniedException.class,
+ () -> gApi.changes().id(result1.getChangeId()).revertSubmission());
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("not permitted: create change on refs/heads/master");
+
+ // assert that if the first repository has no create change permissions and a change from
+ // another repository is trying to revert the submission, it will fail.
+ thrown =
+ assertThrows(
+ PermissionDeniedException.class,
+ () -> gApi.changes().id(result2.getChangeId()).revertSubmission());
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("not permitted: create change on refs/heads/master");
+ }
+
+ @Test
+ @GerritConfig(name = "change.submitWholeTopic", value = "true")
+ public void cantCreateRevertSubmissionWithoutReadPermission() throws Exception {
+ String secondProject = "secondProject";
+ projectOperations.newProject().name(secondProject).create();
+ TestRepository<InMemoryRepository> secondRepo =
+ cloneProject(Project.nameKey("secondProject"), admin);
+ String topic = "topic";
+ PushOneCommit.Result result1 =
+ createChange(testRepo, "master", "first change", "a.txt", "message", topic);
+ PushOneCommit.Result result2 =
+ createChange(secondRepo, "master", "second change", "b.txt", "message", topic);
+ gApi.changes().id(result1.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(result2.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(result1.getChangeId()).revision(result1.getCommit().name()).submit();
+
+ // revoke read permissions for the first repository.
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(block(Permission.READ).ref("refs/heads/master").group(REGISTERED_USERS))
+ .update();
+
+ // assert that if first repository has no read permissions, it will fail.
+ ResourceNotFoundException resourceNotFoundException =
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.changes().id(result1.getChangeId()).revertSubmission());
+ assertThat(resourceNotFoundException)
+ .hasMessageThat()
+ .isEqualTo("Not found: " + result1.getChangeId());
+
+ // assert that if the first repository has no READ permissions and a change from another
+ // repository is trying to revert the submission, it will fail.
+ AuthException authException =
+ assertThrows(
+ AuthException.class, () -> gApi.changes().id(result2.getChangeId()).revertSubmission());
+ assertThat(authException).hasMessageThat().isEqualTo("read not permitted");
+ }
+
+ @Test
+ public void revertSubmissionPreservesReviewersAndCcs() throws Exception {
+ PushOneCommit.Result r = createChange("first change", "a.txt", "message");
+
+ ReviewInput in = ReviewInput.approve();
+ in.reviewer(user.email());
+ in.reviewer(accountCreator.user2().email(), ReviewerState.CC, true);
+ // Add user as reviewer that will create the revert
+ in.reviewer(accountCreator.admin2().email());
+
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in);
+ gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit();
+
+ // expect both the original reviewers and CCs to be preserved
+ // original owner should be added as reviewer, user requesting the revert (new owner) removed
+ requestScopeOperations.setApiUser(accountCreator.admin2().id());
+ Map<ReviewerState, Collection<AccountInfo>> result =
+ getChangeApis(gApi.changes().id(r.getChangeId()).revertSubmission()).get(0).get().reviewers;
+ assertThat(result).containsKey(ReviewerState.REVIEWER);
+
+ List<Integer> reviewers =
+ result.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList());
+ assertThat(result).containsKey(ReviewerState.CC);
+ List<Integer> ccs =
+ result.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList());
+ assertThat(ccs).containsExactly(accountCreator.user2().id().get());
+ assertThat(reviewers).containsExactly(user.id().get(), admin.id().get());
+ }
+
+ @Test
+ public void revertSubmissionNotifications() throws Exception {
+ PushOneCommit.Result firstResult = createChange("first change", "a.txt", "message");
+ approve(firstResult.getChangeId());
+ gApi.changes().id(firstResult.getChangeId()).addReviewer(user.email());
+ PushOneCommit.Result secondResult = createChange("second change", "b.txt", "other");
+ approve(secondResult.getChangeId());
+ gApi.changes().id(secondResult.getChangeId()).addReviewer(user.email());
+
+ gApi.changes()
+ .id(secondResult.getChangeId())
+ .revision(secondResult.getCommit().name())
+ .submit();
+
+ sender.clear();
+ RevertInput revertInput = new RevertInput();
+ revertInput.notify = NotifyHandling.ALL;
+
+ RevertSubmissionInfo revertChanges =
+ gApi.changes().id(secondResult.getChangeId()).revertSubmission(revertInput);
+
+ List<Message> messages = sender.getMessages();
+
+ assertThat(messages).hasSize(4);
+ assertThat(sender.getMessages(revertChanges.revertChanges.get(0).changeId, "newchange"))
+ .hasSize(1);
+ assertThat(sender.getMessages(firstResult.getChangeId(), "revert")).hasSize(1);
+ assertThat(sender.getMessages(revertChanges.revertChanges.get(1).changeId, "newchange"))
+ .hasSize(1);
+ assertThat(sender.getMessages(secondResult.getChangeId(), "revert")).hasSize(1);
+ }
+
+ @Test
+ public void suppressRevertSubmissionNotifications() throws Exception {
+ PushOneCommit.Result firstResult = createChange("first change", "a.txt", "message");
+ approve(firstResult.getChangeId());
+ gApi.changes().id(firstResult.getChangeId()).addReviewer(user.email());
+ PushOneCommit.Result secondResult = createChange("second change", "b.txt", "other");
+ approve(secondResult.getChangeId());
+ gApi.changes().id(secondResult.getChangeId()).addReviewer(user.email());
+
+ gApi.changes()
+ .id(secondResult.getChangeId())
+ .revision(secondResult.getCommit().name())
+ .submit();
+
+ RevertInput revertInput = new RevertInput();
+ revertInput.notify = NotifyHandling.NONE;
+
+ sender.clear();
+ gApi.changes().id(secondResult.getChangeId()).revertSubmission(revertInput);
+ assertThat(sender.getMessages()).isEmpty();
+ }
+
+ @Test
+ public void revertSubmissionOfSingleChange() throws Exception {
+ PushOneCommit.Result result = createChange("Change", "a.txt", "message");
+ approve(result.getChangeId());
+ gApi.changes().id(result.getChangeId()).current().submit();
+ List<ChangeApi> revertChanges =
+ getChangeApis(gApi.changes().id(result.getChangeId()).revertSubmission());
+
+ String sha1Commit = result.getCommit().getName();
+
+ assertThat(revertChanges.get(0).current().commit(false).parents.get(0).commit)
+ .isEqualTo(sha1Commit);
+
+ assertThat(revertChanges.get(0).current().files().get("a.txt").linesDeleted).isEqualTo(1);
+
+ assertThat(revertChanges.get(0).get().revertOf)
+ .isEqualTo(result.getChange().change().getChangeId());
+ assertThat(revertChanges.get(0).get().topic)
+ .startsWith("revert-" + result.getChange().change().getSubmissionId() + "-");
+ }
+
+ @Test
+ public void revertSubmissionWithSetTopic() throws Exception {
+ PushOneCommit.Result result = createChange();
+ gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(result.getChangeId()).topic("topic");
+ gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
+ RevertInput revertInput = new RevertInput();
+ revertInput.topic = "reverted-not-default";
+ assertThat(
+ gApi.changes()
+ .id(result.getChangeId())
+ .revertSubmission(revertInput)
+ .revertChanges
+ .get(0)
+ .topic)
+ .isEqualTo(revertInput.topic);
+ }
+
+ @Test
+ public void revertSubmissionWithSetMessage() throws Exception {
+ PushOneCommit.Result result = createChange();
+ gApi.changes().id(result.getChangeId()).current().review(ReviewInput.approve());
+ gApi.changes().id(result.getChangeId()).revision(result.getCommit().name()).submit();
+ RevertInput revertInput = new RevertInput();
+ revertInput.message = "Message from input";
+ assertThat(
+ gApi.changes()
+ .id(result.getChangeId())
+ .revertSubmission(revertInput)
+ .revertChanges
+ .get(0)
+ .subject)
+ .isEqualTo(revertInput.message);
+ }
+
+ @Test
+ @GerritConfig(name = "change.submitWholeTopic", value = "true")
+ @UseClockStep
+ public void revertSubmissionDifferentRepositoriesWithDependantChange() throws Exception {
+ projectOperations.newProject().name("secondProject").create();
+ TestRepository<InMemoryRepository> secondRepo =
+ cloneProject(Project.nameKey("secondProject"), admin);
+ List<PushOneCommit.Result> resultCommits = new ArrayList<>();
+ String topic = "topic";
+ resultCommits.add(createChange(testRepo, "master", "first change", "a.txt", "message", topic));
+ resultCommits.add(
+ createChange(secondRepo, "master", "first change", "a.txt", "message", topic));
+ resultCommits.add(
+ createChange(secondRepo, "master", "second change", "b.txt", "Other message", topic));
+ for (PushOneCommit.Result result : resultCommits) {
+ approve(result.getChangeId());
+ }
+ // submit all changes
+ gApi.changes().id(resultCommits.get(1).getChangeId()).current().submit();
+ List<ChangeApi> revertChanges =
+ getChangeApis(gApi.changes().id(resultCommits.get(1).getChangeId()).revertSubmission());
+
+ // The reverts are by update time, so the reversal ensures that
+ // revertChanges[i] is the revert of resultCommits[i]
+ Collections.reverse(revertChanges);
+
+ assertThat(revertChanges.get(0).current().files().get("a.txt").linesDeleted).isEqualTo(1);
+ assertThat(revertChanges.get(1).current().files().get("a.txt").linesDeleted).isEqualTo(1);
+ assertThat(revertChanges.get(2).current().files().get("b.txt").linesDeleted).isEqualTo(1);
+ // has size 3 because of the same topic, and submitWholeTopic is true.
+ assertThat(gApi.changes().id(revertChanges.get(0).get()._number).submittedTogether())
+ .hasSize(3);
+
+ // expected messages on source change:
+ // 1. Uploaded patch set 1.
+ // 2. Patch Set 1: Code-Review+2
+ // 3. Change has been successfully merged by Administrator
+ // 4. Created a revert of this change as %s
+
+ for (int i = 0; i < resultCommits.size(); i++) {
+ assertThat(revertChanges.get(i).current().commit(false).parents.get(0).commit)
+ .isEqualTo(resultCommits.get(i).getCommit().getName());
+ assertThat(revertChanges.get(i).get().revertOf)
+ .isEqualTo(resultCommits.get(i).getChange().change().getChangeId());
+ List<ChangeMessageInfo> sourceMessages =
+ new ArrayList<>(gApi.changes().id(resultCommits.get(i).getChangeId()).get().messages);
+ assertThat(sourceMessages).hasSize(4);
+ String expectedMessage =
+ String.format(
+ "Created a revert of this change as %s", revertChanges.get(i).get().changeId);
+ assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage);
+ // Expected message on the created change: "Uploaded patch set 1."
+ List<ChangeMessageInfo> messages =
+ revertChanges.get(i).get().messages.stream().collect(toList());
+ assertThat(messages).hasSize(1);
+ assertThat(messages.get(0).message).isEqualTo("Uploaded patch set 1.");
+ assertThat(revertChanges.get(i).get().revertOf)
+ .isEqualTo(gApi.changes().id(resultCommits.get(i).getChangeId()).get()._number);
+ assertThat(revertChanges.get(i).get().topic)
+ .startsWith("revert-" + resultCommits.get(0).getChange().change().getSubmissionId());
+ }
+ }
+
+ @Test
+ public void cantRevertSubmissionWithAnOpenChange() throws Exception {
+ PushOneCommit.Result result = createChange("first change", "a.txt", "message");
+ approve(result.getChangeId());
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.changes().id(result.getChangeId()).revertSubmission());
+ assertThat(thrown).hasMessageThat().isEqualTo("change is new.");
+ }
+
@Override
protected PushOneCommit.Result createChange() throws Exception {
return createChange("refs/for/master");
@@ -451,4 +795,13 @@
.create();
}
}
+
+ private List<ChangeApi> getChangeApis(RevertSubmissionInfo revertSubmissionInfo)
+ throws Exception {
+ List<ChangeApi> results = new ArrayList<>();
+ for (ChangeInfo changeInfo : revertSubmissionInfo.revertChanges) {
+ results.add(gApi.changes().id(changeInfo._number));
+ }
+ return results;
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
index eebcc5b..1a8790a 100644
--- a/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -49,6 +49,7 @@
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
+import com.google.gerrit.extensions.api.projects.CommentLinkInput;
import com.google.gerrit.extensions.api.projects.ConfigInfo;
import com.google.gerrit.extensions.api.projects.ConfigInput;
import com.google.gerrit.extensions.api.projects.ConfigValue;
@@ -717,6 +718,139 @@
assertCommentLinks(getConfig(), expected);
}
+ @Test
+ public void projectConfigUsesLocallySetCommentlinks() throws Exception {
+ ConfigInput input = new ConfigInput();
+ addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK);
+ addCommentLink(input, JIRA, JIRA_MATCH, JIRA_LINK);
+ ConfigInfo info = setConfig(project, input);
+
+ Map<String, CommentLinkInfo> expected = new HashMap<>();
+ expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK));
+ expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK));
+ assertCommentLinks(info, expected);
+ assertCommentLinks(getConfig(), expected);
+ }
+
+ @Test
+ @GerritConfig(name = "commentlink.bugzilla.match", value = BUGZILLA_MATCH)
+ @GerritConfig(name = "commentlink.bugzilla.link", value = BUGZILLA_LINK)
+ public void projectConfigUsesCommentLinksFromGlobalAndLocal() throws Exception {
+ Map<String, CommentLinkInfo> expected = new HashMap<>();
+ expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK));
+ assertCommentLinks(getConfig(), expected);
+
+ ConfigInput input = new ConfigInput();
+ addCommentLink(input, JIRA, JIRA_MATCH, JIRA_LINK);
+ ConfigInfo info = setConfig(project, input);
+ expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK));
+
+ assertCommentLinks(info, expected);
+ assertCommentLinks(getConfig(), expected);
+ }
+
+ @Test
+ @GerritConfig(name = "commentlink.bugzilla.match", value = BUGZILLA_MATCH)
+ @GerritConfig(name = "commentlink.bugzilla.link", value = BUGZILLA_LINK)
+ public void localCommentLinkOverridesGlobalConfig() throws Exception {
+ String otherLink = "https://other.example.com";
+ ConfigInput input = new ConfigInput();
+ addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, otherLink);
+
+ Map<String, CommentLinkInfo> expected = new HashMap<>();
+ expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, otherLink));
+
+ ConfigInfo info = setConfig(project, input);
+ assertCommentLinks(info, expected);
+ assertCommentLinks(getConfig(), expected);
+ }
+
+ @Test
+ public void localCommentLinksAreInheritedFromParent() throws Exception {
+ ConfigInput input = new ConfigInput();
+ addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK);
+ addCommentLink(input, JIRA, JIRA_MATCH, JIRA_LINK);
+ ConfigInfo info = setConfig(project, input);
+
+ Map<String, CommentLinkInfo> expected = new HashMap<>();
+ expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK));
+ expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK));
+ assertCommentLinks(info, expected);
+
+ Project.NameKey child = projectOperations.newProject().parent(project).create();
+ assertCommentLinks(getConfig(child), expected);
+ }
+
+ @Test
+ public void localCommentLinkOverridesParentCommentLink() throws Exception {
+ ConfigInput input = new ConfigInput();
+ addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK);
+ addCommentLink(input, JIRA, JIRA_MATCH, JIRA_LINK);
+ ConfigInfo info = setConfig(project, input);
+
+ Map<String, CommentLinkInfo> expected = new HashMap<>();
+ expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK));
+ expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK));
+ assertCommentLinks(info, expected);
+
+ Project.NameKey child = projectOperations.newProject().parent(project).create();
+
+ String otherLink = "https://other.example.com";
+ input = new ConfigInput();
+ addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, otherLink);
+ info = setConfig(child, input);
+
+ expected = new HashMap<>();
+ expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, otherLink));
+ expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK));
+
+ assertCommentLinks(getConfig(child), expected);
+ }
+
+ @Test
+ public void updateExistingCommentLink() throws Exception {
+ ConfigInput input = new ConfigInput();
+ addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK);
+ addCommentLink(input, JIRA, JIRA_MATCH, JIRA_LINK);
+ ConfigInfo info = setConfig(project, input);
+
+ Map<String, CommentLinkInfo> expected = new HashMap<>();
+ expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK));
+ expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK));
+ assertCommentLinks(info, expected);
+
+ String otherLink = "https://other.example.com";
+ input = new ConfigInput();
+ addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, otherLink);
+ info = setConfig(project, input);
+
+ expected = new HashMap<>();
+ expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, otherLink));
+ expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK));
+ assertCommentLinks(getConfig(project), expected);
+ }
+
+ @Test
+ public void removeCommentLink() throws Exception {
+ ConfigInput input = new ConfigInput();
+ addCommentLink(input, BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK);
+ addCommentLink(input, JIRA, JIRA_MATCH, JIRA_LINK);
+ ConfigInfo info = setConfig(project, input);
+
+ Map<String, CommentLinkInfo> expected = new HashMap<>();
+ expected.put(BUGZILLA, commentLinkInfo(BUGZILLA, BUGZILLA_MATCH, BUGZILLA_LINK));
+ expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK));
+ assertCommentLinks(info, expected);
+
+ input = new ConfigInput();
+ addCommentLink(input, BUGZILLA, null);
+ info = setConfig(project, input);
+
+ expected = new HashMap<>();
+ expected.put(JIRA, commentLinkInfo(JIRA, JIRA_MATCH, JIRA_LINK));
+ assertCommentLinks(getConfig(project), expected);
+ }
+
private CommentLinkInfo commentLinkInfo(String name, String match, String link) {
return new CommentLinkInfoImpl(name, match, link, null /*html*/, null /*enabled*/);
}
@@ -725,6 +859,21 @@
assertThat(actual.commentlinks).containsExactlyEntriesIn(expected);
}
+ private void addCommentLink(ConfigInput configInput, String name, String match, String link) {
+ CommentLinkInput commentLinkInput = new CommentLinkInput();
+ commentLinkInput.match = match;
+ commentLinkInput.link = link;
+ addCommentLink(configInput, name, commentLinkInput);
+ }
+
+ private void addCommentLink(
+ ConfigInput configInput, String name, CommentLinkInput commentLinkInput) {
+ if (configInput.commentLinks == null) {
+ configInput.commentLinks = new HashMap<>();
+ }
+ configInput.commentLinks.put(name, commentLinkInput);
+ }
+
private ConfigInfo setConfig(Project.NameKey name, ConfigInput input) throws Exception {
return gApi.projects().name(name.get()).config(input);
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
index 8a284d9..83bc3eb 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ChangesRestApiBindingsIT.java
@@ -84,6 +84,7 @@
RestCall.post("/changes/%s/rebase"),
RestCall.post("/changes/%s/restore"),
RestCall.post("/changes/%s/revert"),
+ RestCall.post("/changes/%s/revert_submission"),
RestCall.get("/changes/%s/pure_revert"),
RestCall.post("/changes/%s/submit"),
RestCall.get("/changes/%s/submitted_together"),
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index 48dc89f..02557cc 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -29,6 +29,7 @@
import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
import com.google.gerrit.acceptance.rest.util.RestCall;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.common.data.LabelFunction;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.changes.ReviewInput;
@@ -74,6 +75,7 @@
RestCall.get("/projects/%s/branches"),
RestCall.post("/projects/%s/branches:delete"),
RestCall.put("/projects/%s/branches/new-branch"),
+ RestCall.get("/projects/%s/labels"),
RestCall.get("/projects/%s/tags"),
RestCall.post("/projects/%s/tags:delete"),
RestCall.put("/projects/%s/tags/new-tag"),
@@ -81,7 +83,8 @@
// GET /projects/<project>/branches/<branch>/commits is not implemented
.expectedResponseCode(SC_NOT_FOUND)
.build(),
- RestCall.get("/projects/%s/dashboards"));
+ RestCall.get("/projects/%s/dashboards"),
+ RestCall.put("/projects/%s/labels/new-label"));
/**
* Child project REST endpoints to be tested, each URL contains placeholders for the parent
@@ -159,6 +162,18 @@
private static final ImmutableList<RestCall> COMMIT_FILE_ENDPOINTS =
ImmutableList.of(RestCall.get("/projects/%s/commits/%s/files/%s/content"));
+ /**
+ * Label REST endpoints to be tested, each URL contains placeholders for the project identifier
+ * and the label name.
+ */
+ private static final ImmutableList<RestCall> LABEL_ENDPOINTS =
+ ImmutableList.of(
+ RestCall.get("/projects/%s/labels/%s"),
+ RestCall.put("/projects/%s/labels/%s"),
+
+ // Label deletion must be tested last
+ RestCall.delete("/projects/%s/labels/%s"));
+
private static final String FILENAME = "test.txt";
@Inject private ProjectOperations projectOperations;
@@ -213,6 +228,13 @@
adminRestSession, COMMIT_FILE_ENDPOINTS, project.get(), commit, FILENAME);
}
+ @Test
+ public void labelEndpoints() throws Exception {
+ String label = "Foo-Review";
+ configLabel(label, LabelFunction.NO_OP);
+ RestApiCallHelper.execute(adminRestSession, LABEL_ENDPOINTS, project.get(), label);
+ }
+
private String createAndSubmitChange(String filename) throws Exception {
RevCommit c =
testRepo
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index dda7bbd..8b51e7f 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -63,11 +63,25 @@
return gApi.changes().id(id).revision(1).actions();
}
+ protected Map<String, ActionInfo> getChangeActions(String id) throws Exception {
+ return gApi.changes().id(id).get().actions;
+ }
+
protected String getETag(String id) throws Exception {
return gApi.changes().id(id).current().etag();
}
@Test
+ public void changeActionOneMergedChangeHasReverts() throws Exception {
+ String changeId = createChangeWithTopic().getChangeId();
+ gApi.changes().id(changeId).current().review(ReviewInput.approve());
+ gApi.changes().id(changeId).current().submit();
+ Map<String, ActionInfo> actions = getChangeActions(changeId);
+ assertThat(actions).containsKey("revert");
+ assertThat(actions).containsKey("revert_submission");
+ }
+
+ @Test
public void revisionActionsOneChangePerTopicUnapproved() throws Exception {
String changeId = createChangeWithTopic().getChangeId();
Map<String, ActionInfo> actions = getActions(changeId);
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/BUILD b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
index e082559..b50a12b 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/BUILD
+++ b/javatests/com/google/gerrit/acceptance/rest/project/BUILD
@@ -6,6 +6,7 @@
group = f[:f.index(".")],
labels = ["rest"],
deps = [
+ ":labelassert",
":project",
":push_tag_util",
":refassert",
@@ -14,6 +15,19 @@
) for f in glob(["*IT.java"])]
java_library(
+ name = "labelassert",
+ srcs = [
+ "LabelAssert.java",
+ ],
+ deps = [
+ "//java/com/google/gerrit/common:server",
+ "//java/com/google/gerrit/extensions:api",
+ "//java/com/google/gerrit/server",
+ "//lib/truth",
+ ],
+)
+
+java_library(
name = "refassert",
srcs = [
"RefAssert.java",
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
new file mode 100644
index 0000000..28e8b14
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/CreateLabelIT.java
@@ -0,0 +1,596 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+@NoHttpd
+public class CreateLabelIT extends AbstractDaemonTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ProjectOperations projectOperations;
+
+ @Test
+ public void anonymous() throws Exception {
+ requestScopeOperations.setApiUserAnonymous();
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () ->
+ gApi.projects()
+ .name(project.get())
+ .label("Foo-Review")
+ .create(new LabelDefinitionInput()));
+ assertThat(thrown).hasMessageThat().contains("Authentication required");
+ }
+
+ @Test
+ public void notAllowed() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+ .update();
+
+ requestScopeOperations.setApiUser(user.id());
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () ->
+ gApi.projects()
+ .name(project.get())
+ .label("Foo-Review")
+ .create(new LabelDefinitionInput()));
+ assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted");
+ }
+
+ @Test
+ public void cannotCreateLabelIfNameDoesntMatch() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.name = "Foo";
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("Bar").create(input));
+ assertThat(thrown).hasMessageThat().contains("name in input must match name in URL");
+ }
+
+ @Test
+ public void cannotCreateLabelWithNameThatIsAlreadyInUse() throws Exception {
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () ->
+ gApi.projects()
+ .name(allProjects.get())
+ .label("Code-Review")
+ .create(new LabelDefinitionInput()));
+ assertThat(thrown).hasMessageThat().contains("label Code-Review already exists");
+ }
+
+ @Test
+ public void cannotCreateLabelWithNameThatConflicts() throws Exception {
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () ->
+ gApi.projects()
+ .name(allProjects.get())
+ .label("code-review")
+ .create(new LabelDefinitionInput()));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("label code-review conflicts with existing label Code-Review");
+ }
+
+ @Test
+ public void cannotCreateLabelWithInvalidName() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad");
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("INVALID_NAME").create(input));
+ assertThat(thrown).hasMessageThat().contains("invalid name: INVALID_NAME");
+ }
+
+ @Test
+ public void cannotCreateLabelWithoutValues() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("Foo").create(input));
+ assertThat(thrown).hasMessageThat().contains("values are required");
+
+ input.values = ImmutableMap.of();
+ thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("Foo").create(input));
+ assertThat(thrown).hasMessageThat().contains("values are required");
+ }
+
+ @Test
+ public void cannotCreateLabelWithInvalidValues() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("invalidValue", "description");
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("Foo").create(input));
+ assertThat(thrown).hasMessageThat().contains("invalid value: invalidValue");
+ }
+
+ @Test
+ public void cannotCreateLabelWithValuesThatHaveEmptyDescription() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "");
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("Foo").create(input));
+ assertThat(thrown).hasMessageThat().contains("description for value '+1' cannot be empty");
+ }
+
+ @Test
+ public void cannotCreateLabelWithInvalidDefaultValue() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad");
+ input.defaultValue = 5;
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("Foo").create(input));
+ assertThat(thrown).hasMessageThat().contains("invalid default value: " + input.defaultValue);
+ }
+
+ @Test
+ public void cannotCreateLabelWithUnknownFunction() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad");
+ input.function = "UnknownFuction";
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("Foo").create(input));
+ assertThat(thrown).hasMessageThat().contains("unknown function: " + input.function);
+ }
+
+ @Test
+ public void cannotCreateLabelWithInvalidBranch() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", "0", "Don't Know", "-1", "Looks Bad");
+ input.branches = ImmutableList.of("refs heads master");
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(project.get()).label("Foo").create(input));
+ assertThat(thrown).hasMessageThat().contains("invalid branch: refs heads master");
+ }
+
+ @Test
+ public void createWithNameAndValuesOnly() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("Foo").create(input).get();
+
+ assertThat(createdLabel.name).isEqualTo("Foo");
+ assertThat(createdLabel.projectName).isEqualTo(project.get());
+ assertThat(createdLabel.function).isEqualTo(LabelFunction.MAX_WITH_BLOCK.getFunctionName());
+ assertThat(createdLabel.values).containsExactlyEntriesIn(input.values);
+ assertThat(createdLabel.defaultValue).isEqualTo(0);
+ assertThat(createdLabel.branches).isNull();
+ assertThat(createdLabel.canOverride).isTrue();
+ assertThat(createdLabel.copyAnyScore).isNull();
+ assertThat(createdLabel.copyMinScore).isNull();
+ assertThat(createdLabel.copyMaxScore).isNull();
+ assertThat(createdLabel.copyAllScoresIfNoChange).isTrue();
+ assertThat(createdLabel.copyAllScoresIfNoCodeChange).isNull();
+ assertThat(createdLabel.copyAllScoresOnTrivialRebase).isNull();
+ assertThat(createdLabel.copyAllScoresOnMergeFirstParentUpdate).isNull();
+ assertThat(createdLabel.allowPostSubmit).isTrue();
+ assertThat(createdLabel.ignoreSelfApproval).isNull();
+ }
+
+ @Test
+ public void createWithFunction() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.function = LabelFunction.NO_OP.getFunctionName();
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("Foo").create(input).get();
+
+ assertThat(createdLabel.function).isEqualTo(LabelFunction.NO_OP.getFunctionName());
+ }
+
+ @Test
+ public void functionEmptyAfterTrim() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.function = " ";
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("Foo").create(input).get();
+
+ assertThat(createdLabel.function).isEqualTo(LabelFunction.MAX_WITH_BLOCK.getFunctionName());
+ }
+
+ @Test
+ public void valuesAndDescriptionsAreTrimmed() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ // Positive values can be specified as '<value>' or '+<value>'.
+ input.values =
+ ImmutableMap.of(
+ " 2 ",
+ " Looks Very Good ",
+ " +1 ",
+ " Looks Good ",
+ " 0 ",
+ " Don't Know ",
+ " -1 ",
+ " Looks Bad ",
+ " -2 ",
+ " Looks Very Bad ");
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("Foo").create(input).get();
+ assertThat(createdLabel.values)
+ .containsExactly(
+ "+2", "Looks Very Good",
+ "+1", "Looks Good",
+ " 0", "Don't Know",
+ "-1", "Looks Bad",
+ "-2", "Looks Very Bad");
+ }
+
+ @Test
+ public void createWithDefaultValue() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.defaultValue = 1;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("Foo").create(input).get();
+
+ assertThat(createdLabel.defaultValue).isEqualTo(input.defaultValue);
+ }
+
+ @Test
+ public void createWithBranches() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ // Branches can be full ref, ref pattern or regular expression.
+ input.branches =
+ ImmutableList.of("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*");
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("Foo").create(input).get();
+ assertThat(createdLabel.branches).containsExactlyElementsIn(input.branches);
+ }
+
+ @Test
+ public void branchesAreTrimmed() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.branches =
+ ImmutableList.of(" refs/heads/master ", " refs/heads/foo/* ", " ^refs/heads/stable-.* ");
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("Foo").create(input).get();
+ assertThat(createdLabel.branches)
+ .containsExactly("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*");
+ }
+
+ @Test
+ public void emptyBranchesAreIgnored() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.branches = ImmutableList.of("refs/heads/master", "", " ");
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("Foo").create(input).get();
+ assertThat(createdLabel.branches).containsExactly("refs/heads/master");
+ }
+
+ @Test
+ public void branchesAreAutomaticallyPrefixedWithRefsHeads() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.branches = ImmutableList.of("master", "refs/meta/config");
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("Foo").create(input).get();
+ assertThat(createdLabel.branches).containsExactly("refs/heads/master", "refs/meta/config");
+ }
+
+ @Test
+ public void createWithCanOverride() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.canOverride = true;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.canOverride).isTrue();
+ }
+
+ @Test
+ public void createWithoutCanOverride() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.canOverride = false;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.canOverride).isNull();
+ }
+
+ @Test
+ public void createWithCopyAnyScore() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyAnyScore = true;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyAnyScore).isTrue();
+ }
+
+ @Test
+ public void createWithoutCopyAnyScore() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyAnyScore = false;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyAnyScore).isNull();
+ }
+
+ @Test
+ public void createWithCopyMinScore() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyMinScore = true;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyMinScore).isTrue();
+ }
+
+ @Test
+ public void createWithoutCopyMinScore() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyMinScore = false;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyMinScore).isNull();
+ }
+
+ @Test
+ public void createWithCopyMaxScore() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyMaxScore = true;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyMaxScore).isTrue();
+ }
+
+ @Test
+ public void createWithoutCopyMaxScore() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyMaxScore = false;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyMaxScore).isNull();
+ }
+
+ @Test
+ public void createWithCopyAllScoresIfNoChange() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyAllScoresIfNoChange = true;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyAllScoresIfNoChange).isTrue();
+ }
+
+ @Test
+ public void createWithoutCopyAllScoresIfNoChange() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyAllScoresIfNoChange = false;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyAllScoresIfNoChange).isNull();
+ }
+
+ @Test
+ public void createWithCopyAllScoresIfNoCodeChange() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyAllScoresIfNoCodeChange = true;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyAllScoresIfNoCodeChange).isTrue();
+ }
+
+ @Test
+ public void createWithoutCopyAllScoresIfNoCodeChange() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyAllScoresIfNoCodeChange = false;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyAllScoresIfNoCodeChange).isNull();
+ }
+
+ @Test
+ public void createWithCopyAllScoresOnTrivialRebase() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyAllScoresOnTrivialRebase = true;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyAllScoresOnTrivialRebase).isTrue();
+ }
+
+ @Test
+ public void createWithoutCopyAllScoresOnTrivialRebase() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyAllScoresOnTrivialRebase = false;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyAllScoresOnTrivialRebase).isNull();
+ }
+
+ @Test
+ public void createWithCopyAllScoresOnMergeFirstParentUpdate() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyAllScoresOnMergeFirstParentUpdate = true;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyAllScoresOnMergeFirstParentUpdate).isTrue();
+ }
+
+ @Test
+ public void createWithoutCopyAllScoresOnMergeFirstParentUpdate() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.copyAllScoresOnMergeFirstParentUpdate = false;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.copyAllScoresOnMergeFirstParentUpdate).isNull();
+ }
+
+ @Test
+ public void createWithAllowPostSubmit() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.allowPostSubmit = true;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.allowPostSubmit).isTrue();
+ }
+
+ @Test
+ public void createWithoutAllowPostSubmit() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.allowPostSubmit = false;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.allowPostSubmit).isNull();
+ }
+
+ @Test
+ public void createWithIgnoreSelfApproval() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.ignoreSelfApproval = true;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.ignoreSelfApproval).isTrue();
+ }
+
+ @Test
+ public void createWithoutIgnoreSelfApproval() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.ignoreSelfApproval = false;
+
+ LabelDefinitionInfo createdLabel =
+ gApi.projects().name(project.get()).label("foo").create(input).get();
+ assertThat(createdLabel.ignoreSelfApproval).isNull();
+ }
+
+ @Test
+ public void defaultCommitMessage() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ gApi.projects().name(project.get()).label("Foo").create(input);
+ assertThat(projectOperations.project(project).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo("Update label");
+ }
+
+ @Test
+ public void withCommitMessage() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.commitMessage = "Add Foo Label";
+ gApi.projects().name(project.get()).label("Foo").create(input);
+ assertThat(projectOperations.project(project).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo(input.commitMessage);
+ }
+
+ @Test
+ public void commitMessageIsTrimmed() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "Looks Good", " 0", "Don't Know", "-1", "Looks Bad");
+ input.commitMessage = " Add Foo Label ";
+ gApi.projects().name(project.get()).label("Foo").create(input);
+ assertThat(projectOperations.project(project).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo("Add Foo Label");
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java
new file mode 100644
index 0000000..c916285
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/DeleteLabelIT.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+public class DeleteLabelIT extends AbstractDaemonTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ProjectOperations projectOperations;
+
+ @Test
+ public void anonymous() throws Exception {
+ requestScopeOperations.setApiUserAnonymous();
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").delete());
+ assertThat(thrown).hasMessageThat().contains("Authentication required");
+ }
+
+ @Test
+ public void notAllowed() throws Exception {
+ projectOperations
+ .project(allProjects)
+ .forUpdate()
+ .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+ .update();
+
+ requestScopeOperations.setApiUser(user.id());
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").delete());
+ assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted");
+ }
+
+ @Test
+ public void nonExisting() throws Exception {
+ ResourceNotFoundException thrown =
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Non-Existing-Review").delete());
+ assertThat(thrown).hasMessageThat().contains("Not found: Non-Existing-Review");
+ }
+
+ @Test
+ public void delete() throws Exception {
+ gApi.projects().name(allProjects.get()).label("Code-Review").delete();
+
+ ResourceNotFoundException thrown =
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.projects().name(project.get()).label("Code-Review").get());
+ assertThat(thrown).hasMessageThat().contains("Not found: Code-Review");
+ }
+
+ @Test
+ public void defaultCommitMessage() throws Exception {
+ gApi.projects().name(allProjects.get()).label("Code-Review").delete();
+ assertThat(
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo("Delete label");
+ }
+
+ @Test
+ public void withCommitMessage() throws Exception {
+ gApi.projects().name(allProjects.get()).label("Code-Review").delete("Delete Code-Review label");
+ assertThat(
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo("Delete Code-Review label");
+ }
+
+ @Test
+ public void commitMessageIsTrimmed() throws Exception {
+ gApi.projects()
+ .name(allProjects.get())
+ .label("Code-Review")
+ .delete(" Delete Code-Review label ");
+ assertThat(
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo("Delete Code-Review label");
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/GetLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/GetLabelIT.java
new file mode 100644
index 0000000..9f98490
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/GetLabelIT.java
@@ -0,0 +1,154 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+@NoHttpd
+public class GetLabelIT extends AbstractDaemonTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+
+ @Test
+ public void anonymous() throws Exception {
+ requestScopeOperations.setApiUserAnonymous();
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").get());
+ assertThat(thrown).hasMessageThat().contains("Authentication required");
+ }
+
+ @Test
+ public void notAllowed() throws Exception {
+ requestScopeOperations.setApiUser(user.id());
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").get());
+ assertThat(thrown).hasMessageThat().contains("read refs/meta/config not permitted");
+ }
+
+ @Test
+ public void notFound() throws Exception {
+ ResourceNotFoundException thrown =
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.projects().name(project.get()).label("Foo-Review").get());
+ assertThat(thrown).hasMessageThat().contains("Not found: Foo-Review");
+ }
+
+ @Test
+ public void allProjectsCodeReviewLabel() throws Exception {
+ LabelDefinitionInfo codeReviewLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").get();
+ LabelAssert.assertCodeReviewLabel(codeReviewLabel);
+ }
+
+ @Test
+ public void labelWithDefaultValue() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+
+ // set default value
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setDefaultValue((short) 1);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+
+ LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("foo").get();
+ assertThat(fooLabel.defaultValue).isEqualTo(1);
+ }
+
+ @Test
+ public void labelLimitedToBranches() throws Exception {
+ configLabel(
+ "foo", LabelFunction.NO_OP, ImmutableList.of("refs/heads/master", "^refs/heads/stable-.*"));
+
+ LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("foo").get();
+ assertThat(fooLabel.branches).containsExactly("refs/heads/master", "^refs/heads/stable-.*");
+ }
+
+ @Test
+ public void labelWithoutRules() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+
+ // unset rules which are enabled by default
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setCanOverride(false);
+ labelType.setCopyAllScoresIfNoChange(false);
+ labelType.setAllowPostSubmit(false);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+
+ LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("foo").get();
+ assertThat(fooLabel.canOverride).isNull();
+ assertThat(fooLabel.copyAnyScore).isNull();
+ assertThat(fooLabel.copyMinScore).isNull();
+ assertThat(fooLabel.copyMaxScore).isNull();
+ assertThat(fooLabel.copyAllScoresIfNoChange).isNull();
+ assertThat(fooLabel.copyAllScoresIfNoCodeChange).isNull();
+ assertThat(fooLabel.copyAllScoresOnTrivialRebase).isNull();
+ assertThat(fooLabel.copyAllScoresOnMergeFirstParentUpdate).isNull();
+ assertThat(fooLabel.allowPostSubmit).isNull();
+ assertThat(fooLabel.ignoreSelfApproval).isNull();
+ }
+
+ @Test
+ public void labelWithAllRules() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+
+ // set rules which are not enabled by default
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setCopyAnyScore(true);
+ labelType.setCopyMinScore(true);
+ labelType.setCopyMaxScore(true);
+ labelType.setCopyAllScoresIfNoCodeChange(true);
+ labelType.setCopyAllScoresOnTrivialRebase(true);
+ labelType.setCopyAllScoresOnMergeFirstParentUpdate(true);
+ labelType.setIgnoreSelfApproval(true);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+
+ LabelDefinitionInfo fooLabel = gApi.projects().name(project.get()).label("foo").get();
+ assertThat(fooLabel.canOverride).isTrue();
+ assertThat(fooLabel.copyAnyScore).isTrue();
+ assertThat(fooLabel.copyMinScore).isTrue();
+ assertThat(fooLabel.copyMaxScore).isTrue();
+ assertThat(fooLabel.copyAllScoresIfNoChange).isTrue();
+ assertThat(fooLabel.copyAllScoresIfNoCodeChange).isTrue();
+ assertThat(fooLabel.copyAllScoresOnTrivialRebase).isTrue();
+ assertThat(fooLabel.copyAllScoresOnMergeFirstParentUpdate).isTrue();
+ assertThat(fooLabel.allowPostSubmit).isTrue();
+ assertThat(fooLabel.ignoreSelfApproval).isTrue();
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/LabelAssert.java b/javatests/com/google/gerrit/acceptance/rest/project/LabelAssert.java
new file mode 100644
index 0000000..7998ecb
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/LabelAssert.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
+
+public class LabelAssert {
+ public static void assertCodeReviewLabel(LabelDefinitionInfo codeReviewLabel) {
+ assertThat(codeReviewLabel.name).isEqualTo("Code-Review");
+ assertThat(codeReviewLabel.projectName).isEqualTo(AllProjectsNameProvider.DEFAULT);
+ assertThat(codeReviewLabel.function).isEqualTo(LabelFunction.MAX_WITH_BLOCK.getFunctionName());
+ assertThat(codeReviewLabel.values)
+ .containsExactly(
+ "+2",
+ "Looks good to me, approved",
+ "+1",
+ "Looks good to me, but someone else must approve",
+ " 0",
+ "No score",
+ "-1",
+ "I would prefer this is not merged as is",
+ "-2",
+ "This shall not be merged");
+ assertThat(codeReviewLabel.defaultValue).isEqualTo(0);
+ assertThat(codeReviewLabel.branches).isNull();
+ assertThat(codeReviewLabel.canOverride).isTrue();
+ assertThat(codeReviewLabel.copyAnyScore).isNull();
+ assertThat(codeReviewLabel.copyMinScore).isTrue();
+ assertThat(codeReviewLabel.copyMaxScore).isNull();
+ assertThat(codeReviewLabel.copyAllScoresIfNoChange).isTrue();
+ assertThat(codeReviewLabel.copyAllScoresIfNoCodeChange).isNull();
+ assertThat(codeReviewLabel.copyAllScoresOnTrivialRebase).isTrue();
+ assertThat(codeReviewLabel.copyAllScoresOnMergeFirstParentUpdate).isNull();
+ assertThat(codeReviewLabel.allowPostSubmit).isTrue();
+ assertThat(codeReviewLabel.ignoreSelfApproval).isNull();
+ }
+
+ private LabelAssert() {}
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java
new file mode 100644
index 0000000..d2539e5
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListLabelsIT.java
@@ -0,0 +1,268 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.inject.Inject;
+import java.util.List;
+import org.junit.Test;
+
+@NoHttpd
+public class ListLabelsIT extends AbstractDaemonTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ProjectOperations projectOperations;
+
+ @Test
+ public void anonymous() throws Exception {
+ requestScopeOperations.setApiUserAnonymous();
+ AuthException thrown =
+ assertThrows(AuthException.class, () -> gApi.projects().name(project.get()).labels().get());
+ assertThat(thrown).hasMessageThat().contains("Authentication required");
+ }
+
+ @Test
+ public void notAllowed() throws Exception {
+ requestScopeOperations.setApiUser(user.id());
+ AuthException thrown =
+ assertThrows(AuthException.class, () -> gApi.projects().name(project.get()).labels().get());
+ assertThat(thrown).hasMessageThat().contains("read refs/meta/config not permitted");
+ }
+
+ @Test
+ public void noLabels() throws Exception {
+ assertThat(gApi.projects().name(project.get()).labels().get()).isEmpty();
+ }
+
+ @Test
+ public void allProjectsLabels() throws Exception {
+ List<LabelDefinitionInfo> labels = gApi.projects().name(allProjects.get()).labels().get();
+ assertThat(labelNames(labels)).containsExactly("Code-Review");
+
+ LabelDefinitionInfo codeReviewLabel = Iterables.getOnlyElement(labels);
+ LabelAssert.assertCodeReviewLabel(codeReviewLabel);
+ }
+
+ @Test
+ public void labelsAreSortedByName() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ configLabel("bar", LabelFunction.NO_OP);
+ configLabel("baz", LabelFunction.NO_OP);
+
+ List<LabelDefinitionInfo> labels = gApi.projects().name(project.get()).labels().get();
+ assertThat(labelNames(labels)).containsExactly("bar", "baz", "foo").inOrder();
+ }
+
+ @Test
+ public void labelWithDefaultValue() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+
+ // set default value
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setDefaultValue((short) 1);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+
+ List<LabelDefinitionInfo> labels = gApi.projects().name(project.get()).labels().get();
+ assertThat(labelNames(labels)).containsExactly("foo");
+
+ LabelDefinitionInfo fooLabel = Iterables.getOnlyElement(labels);
+ assertThat(fooLabel.defaultValue).isEqualTo(1);
+ }
+
+ @Test
+ public void labelLimitedToBranches() throws Exception {
+ configLabel(
+ "foo", LabelFunction.NO_OP, ImmutableList.of("refs/heads/master", "^refs/heads/stable-.*"));
+
+ List<LabelDefinitionInfo> labels = gApi.projects().name(project.get()).labels().get();
+ assertThat(labelNames(labels)).containsExactly("foo");
+
+ LabelDefinitionInfo fooLabel = Iterables.getOnlyElement(labels);
+ assertThat(fooLabel.branches).containsExactly("refs/heads/master", "^refs/heads/stable-.*");
+ }
+
+ @Test
+ public void labelWithoutRules() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+
+ // unset rules which are enabled by default
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setCanOverride(false);
+ labelType.setCopyAllScoresIfNoChange(false);
+ labelType.setAllowPostSubmit(false);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+
+ List<LabelDefinitionInfo> labels = gApi.projects().name(project.get()).labels().get();
+ assertThat(labelNames(labels)).containsExactly("foo");
+
+ LabelDefinitionInfo fooLabel = Iterables.getOnlyElement(labels);
+ assertThat(fooLabel.canOverride).isNull();
+ assertThat(fooLabel.copyAnyScore).isNull();
+ assertThat(fooLabel.copyMinScore).isNull();
+ assertThat(fooLabel.copyMaxScore).isNull();
+ assertThat(fooLabel.copyAllScoresIfNoChange).isNull();
+ assertThat(fooLabel.copyAllScoresIfNoCodeChange).isNull();
+ assertThat(fooLabel.copyAllScoresOnTrivialRebase).isNull();
+ assertThat(fooLabel.copyAllScoresOnMergeFirstParentUpdate).isNull();
+ assertThat(fooLabel.allowPostSubmit).isNull();
+ assertThat(fooLabel.ignoreSelfApproval).isNull();
+ }
+
+ @Test
+ public void labelWithAllRules() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+
+ // set rules which are not enabled by default
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setCopyAnyScore(true);
+ labelType.setCopyMinScore(true);
+ labelType.setCopyMaxScore(true);
+ labelType.setCopyAllScoresIfNoCodeChange(true);
+ labelType.setCopyAllScoresOnTrivialRebase(true);
+ labelType.setCopyAllScoresOnMergeFirstParentUpdate(true);
+ labelType.setIgnoreSelfApproval(true);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+
+ List<LabelDefinitionInfo> labels = gApi.projects().name(project.get()).labels().get();
+ assertThat(labelNames(labels)).containsExactly("foo");
+
+ LabelDefinitionInfo fooLabel = Iterables.getOnlyElement(labels);
+ assertThat(fooLabel.canOverride).isTrue();
+ assertThat(fooLabel.copyAnyScore).isTrue();
+ assertThat(fooLabel.copyMinScore).isTrue();
+ assertThat(fooLabel.copyMaxScore).isTrue();
+ assertThat(fooLabel.copyAllScoresIfNoChange).isTrue();
+ assertThat(fooLabel.copyAllScoresIfNoCodeChange).isTrue();
+ assertThat(fooLabel.copyAllScoresOnTrivialRebase).isTrue();
+ assertThat(fooLabel.copyAllScoresOnMergeFirstParentUpdate).isTrue();
+ assertThat(fooLabel.allowPostSubmit).isTrue();
+ assertThat(fooLabel.ignoreSelfApproval).isTrue();
+ }
+
+ @Test
+ public void withInheritedLabelsNotAllowed() throws Exception {
+ projectOperations
+ .project(project)
+ .forUpdate()
+ .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+ .update();
+
+ requestScopeOperations.setApiUser(user.id());
+
+ // can list labels without inheritance
+ gApi.projects().name(project.get()).labels().get();
+
+ // cannot list labels with inheritance
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () -> gApi.projects().name(project.get()).labels().withInherited(true).get());
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("All-Projects: read refs/meta/config not permitted");
+ }
+
+ @Test
+ public void inheritedLabelsOnly() throws Exception {
+ List<LabelDefinitionInfo> labels =
+ gApi.projects().name(project.get()).labels().withInherited(true).get();
+ assertThat(labelNames(labels)).containsExactly("Code-Review");
+
+ LabelDefinitionInfo codeReviewLabel = Iterables.getOnlyElement(labels);
+ LabelAssert.assertCodeReviewLabel(codeReviewLabel);
+ }
+
+ @Test
+ public void withInheritedLabels() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ configLabel("bar", LabelFunction.NO_OP);
+ configLabel("baz", LabelFunction.NO_OP);
+
+ List<LabelDefinitionInfo> labels =
+ gApi.projects().name(project.get()).labels().withInherited(true).get();
+ assertThat(labelNames(labels)).containsExactly("Code-Review", "bar", "baz", "foo").inOrder();
+
+ LabelAssert.assertCodeReviewLabel(labels.get(0));
+ assertThat(labels.get(1).name).isEqualTo("bar");
+ assertThat(labels.get(1).projectName).isEqualTo(project.get());
+ assertThat(labels.get(2).name).isEqualTo("baz");
+ assertThat(labels.get(2).projectName).isEqualTo(project.get());
+ assertThat(labels.get(3).name).isEqualTo("foo");
+ assertThat(labels.get(3).projectName).isEqualTo(project.get());
+ }
+
+ @Test
+ public void withInheritedLabelsAndOverriddenLabel() throws Exception {
+ configLabel("Code-Review", LabelFunction.NO_OP);
+
+ List<LabelDefinitionInfo> labels =
+ gApi.projects().name(project.get()).labels().withInherited(true).get();
+ assertThat(labelNames(labels)).containsExactly("Code-Review", "Code-Review");
+
+ LabelAssert.assertCodeReviewLabel(labels.get(0));
+ assertThat(labels.get(1).name).isEqualTo("Code-Review");
+ assertThat(labels.get(1).projectName).isEqualTo(project.get());
+ assertThat(labels.get(1).function).isEqualTo(LabelFunction.NO_OP.getFunctionName());
+ }
+
+ @Test
+ public void withInheritedLabelsFromMultipleParents() throws Exception {
+ configLabel(project, "foo", LabelFunction.NO_OP);
+
+ Project.NameKey childProject =
+ projectOperations.newProject().name("child").parent(project).create();
+ configLabel(childProject, "bar", LabelFunction.NO_OP);
+
+ List<LabelDefinitionInfo> labels =
+ gApi.projects().name(childProject.get()).labels().withInherited(true).get();
+ assertThat(labelNames(labels)).containsExactly("Code-Review", "foo", "bar").inOrder();
+
+ LabelAssert.assertCodeReviewLabel(labels.get(0));
+ assertThat(labels.get(1).name).isEqualTo("foo");
+ assertThat(labels.get(1).projectName).isEqualTo(project.get());
+ assertThat(labels.get(2).name).isEqualTo("bar");
+ assertThat(labels.get(2).projectName).isEqualTo(childProject.get());
+ }
+
+ private static List<String> labelNames(List<LabelDefinitionInfo> labels) {
+ return labels.stream().map(l -> l.name).collect(toList());
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java b/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
new file mode 100644
index 0000000..9cba930
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/project/SetLabelIT.java
@@ -0,0 +1,880 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
+import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
+import com.google.gerrit.common.data.LabelFunction;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.common.LabelDefinitionInfo;
+import com.google.gerrit.extensions.common.LabelDefinitionInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.inject.Inject;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+
+@NoHttpd
+public class SetLabelIT extends AbstractDaemonTest {
+ @Inject private RequestScopeOperations requestScopeOperations;
+ @Inject private ProjectOperations projectOperations;
+
+ @Test
+ public void anonymous() throws Exception {
+ requestScopeOperations.setApiUserAnonymous();
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () ->
+ gApi.projects()
+ .name(allProjects.get())
+ .label("Code-Review")
+ .update(new LabelDefinitionInput()));
+ assertThat(thrown).hasMessageThat().contains("Authentication required");
+ }
+
+ @Test
+ public void notAllowed() throws Exception {
+ projectOperations
+ .project(allProjects)
+ .forUpdate()
+ .add(allow(Permission.READ).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
+ .update();
+
+ requestScopeOperations.setApiUser(user.id());
+ AuthException thrown =
+ assertThrows(
+ AuthException.class,
+ () ->
+ gApi.projects()
+ .name(allProjects.get())
+ .label("Code-Review")
+ .update(new LabelDefinitionInput()));
+ assertThat(thrown).hasMessageThat().contains("write refs/meta/config not permitted");
+ }
+
+ @Test
+ public void updateName() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.name = "Foo-Review";
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(updatedLabel.name).isEqualTo(input.name);
+
+ assertThat(gApi.projects().name(allProjects.get()).label("Foo-Review").get()).isNotNull();
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").get());
+ }
+
+ @Test
+ public void nameIsTrimmed() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.name = " Foo-Review ";
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(updatedLabel.name).isEqualTo("Foo-Review");
+
+ assertThat(gApi.projects().name(allProjects.get()).label("Foo-Review").get()).isNotNull();
+ assertThrows(
+ ResourceNotFoundException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").get());
+ }
+
+ @Test
+ public void cannotSetEmptyName() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.name = "";
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input));
+ assertThat(thrown).hasMessageThat().contains("name cannot be empty");
+ }
+
+ @Test
+ public void cannotSetInvalidName() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.name = "INVALID_NAME";
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input));
+ assertThat(thrown).hasMessageThat().contains("invalid name: " + input.name);
+ }
+
+ @Test
+ public void cannotSetNameIfNameClashes() throws Exception {
+ configLabel("Foo-Review", LabelFunction.NO_OP);
+ configLabel("Bar-Review", LabelFunction.NO_OP);
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.name = "Bar-Review";
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.projects().name(project.get()).label("Foo-Review").update(input));
+ assertThat(thrown).hasMessageThat().contains("name " + input.name + " already in use");
+ }
+
+ @Test
+ public void cannotSetNameIfNameConflicts() throws Exception {
+ configLabel("Foo-Review", LabelFunction.NO_OP);
+ configLabel("Bar-Review", LabelFunction.NO_OP);
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.name = "bar-review";
+
+ ResourceConflictException thrown =
+ assertThrows(
+ ResourceConflictException.class,
+ () -> gApi.projects().name(project.get()).label("Foo-Review").update(input));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("name bar-review conflicts with existing label Bar-Review");
+ }
+
+ @Test
+ public void updateFunction() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.function = LabelFunction.NO_OP.getFunctionName();
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(updatedLabel.function).isEqualTo(input.function);
+
+ assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().function)
+ .isEqualTo(input.function);
+ }
+
+ @Test
+ public void functionIsTrimmed() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.function = " " + LabelFunction.NO_OP.getFunctionName() + " ";
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(updatedLabel.function).isEqualTo(LabelFunction.NO_OP.getFunctionName());
+
+ assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().function)
+ .isEqualTo(LabelFunction.NO_OP.getFunctionName());
+ }
+
+ @Test
+ public void cannotSetEmptyFunction() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.function = "";
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input));
+ assertThat(thrown).hasMessageThat().contains("function cannot be empty");
+ }
+
+ @Test
+ public void cannotSetUnknownFunction() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.function = "UnknownFunction";
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input));
+ assertThat(thrown).hasMessageThat().contains("unknown function: " + input.function);
+ }
+
+ @Test
+ public void cannotSetEmptyValues() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of();
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input));
+ assertThat(thrown).hasMessageThat().contains("values cannot be empty");
+ }
+
+ @Test
+ public void updateValues() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ // Positive values can be specified as '<value>' or '+<value>'.
+ input.values =
+ ImmutableMap.of(
+ "2",
+ "Looks Very Good",
+ "+1",
+ "Looks Good",
+ "0",
+ "Don't Know",
+ "-1",
+ "Looks Bad",
+ "-2",
+ "Looks Very Bad");
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(updatedLabel.values)
+ .containsExactly(
+ "+2", "Looks Very Good",
+ "+1", "Looks Good",
+ " 0", "Don't Know",
+ "-1", "Looks Bad",
+ "-2", "Looks Very Bad");
+
+ assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().values)
+ .containsExactly(
+ "+2", "Looks Very Good",
+ "+1", "Looks Good",
+ " 0", "Don't Know",
+ "-1", "Looks Bad",
+ "-2", "Looks Very Bad");
+ }
+
+ @Test
+ public void valuesAndDescriptionsAreTrimmed() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ // Positive values can be specified as '<value>' or '+<value>'.
+ input.values =
+ ImmutableMap.of(
+ " 2 ",
+ " Looks Very Good ",
+ " +1 ",
+ " Looks Good ",
+ " 0 ",
+ " Don't Know ",
+ " -1 ",
+ " Looks Bad ",
+ " -2 ",
+ " Looks Very Bad ");
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(updatedLabel.values)
+ .containsExactly(
+ "+2", "Looks Very Good",
+ "+1", "Looks Good",
+ " 0", "Don't Know",
+ "-1", "Looks Bad",
+ "-2", "Looks Very Bad");
+
+ assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().values)
+ .containsExactly(
+ "+2", "Looks Very Good",
+ "+1", "Looks Good",
+ " 0", "Don't Know",
+ "-1", "Looks Bad",
+ "-2", "Looks Very Bad");
+ }
+
+ @Test
+ public void cannotSetInvalidValues() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("invalidValue", "description");
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input));
+ assertThat(thrown).hasMessageThat().contains("invalid value: invalidValue");
+ }
+
+ @Test
+ public void cannotSetValueWithEmptyDescription() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.values = ImmutableMap.of("+1", "");
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input));
+ assertThat(thrown).hasMessageThat().contains("description for value '+1' cannot be empty");
+ }
+
+ @Test
+ public void updateDefaultValue() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.defaultValue = 1;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(updatedLabel.defaultValue).isEqualTo(input.defaultValue);
+
+ assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().defaultValue)
+ .isEqualTo(input.defaultValue);
+ }
+
+ @Test
+ public void cannotSetInvalidDefaultValue() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.defaultValue = 5;
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input));
+ assertThat(thrown).hasMessageThat().contains("invalid default value: " + input.defaultValue);
+ }
+
+ @Test
+ public void updateBranches() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ // Branches can be full ref, ref pattern or regular expression.
+ input.branches =
+ ImmutableList.of("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*");
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(updatedLabel.branches).containsExactlyElementsIn(input.branches);
+
+ assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().branches)
+ .containsExactlyElementsIn(input.branches);
+ }
+
+ @Test
+ public void branchesAreTrimmed() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.branches =
+ ImmutableList.of(" refs/heads/master ", " refs/heads/foo/* ", " ^refs/heads/stable-.* ");
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(updatedLabel.branches)
+ .containsExactly("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*");
+
+ assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().branches)
+ .containsExactly("refs/heads/master", "refs/heads/foo/*", "^refs/heads/stable-.*");
+ }
+
+ @Test
+ public void emptyBranchesAreIgnored() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.branches = ImmutableList.of("refs/heads/master", "", " ");
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(updatedLabel.branches).containsExactly("refs/heads/master");
+
+ assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().branches)
+ .containsExactly("refs/heads/master");
+ }
+
+ @Test
+ public void branchesCanBeUnset() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.branches = ImmutableList.of("refs/heads/master");
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().branches)
+ .isNotNull();
+
+ input.branches = ImmutableList.of();
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(updatedLabel.branches).isNull();
+ assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().branches)
+ .isNull();
+ }
+
+ @Test
+ public void cannotSetInvalidBranch() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.branches = ImmutableList.of("refs heads master");
+
+ BadRequestException thrown =
+ assertThrows(
+ BadRequestException.class,
+ () -> gApi.projects().name(allProjects.get()).label("Code-Review").update(input));
+ assertThat(thrown).hasMessageThat().contains("invalid branch: refs heads master");
+ }
+
+ @Test
+ public void branchesAreAutomaticallyPrefixedWithRefsHeads() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.branches = ImmutableList.of("master", "refs/meta/config");
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(updatedLabel.branches).containsExactly("refs/heads/master", "refs/meta/config");
+
+ assertThat(gApi.projects().name(allProjects.get()).label("Code-Review").get().branches)
+ .containsExactly("refs/heads/master", "refs/meta/config");
+ }
+
+ @Test
+ public void setCanOverride() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setCanOverride(false);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+ assertThat(gApi.projects().name(project.get()).label("foo").get().canOverride).isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.canOverride = true;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.canOverride).isTrue();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().canOverride).isTrue();
+ }
+
+ @Test
+ public void unsetCanOverride() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).label("foo").get().canOverride).isTrue();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.canOverride = false;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.canOverride).isNull();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().canOverride).isNull();
+ }
+
+ @Test
+ public void setCopyAnyScore() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAnyScore).isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyAnyScore = true;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyAnyScore).isTrue();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAnyScore).isTrue();
+ }
+
+ @Test
+ public void unsetCopyAnyScore() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setCopyAnyScore(true);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAnyScore).isTrue();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyAnyScore = false;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyAnyScore).isNull();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAnyScore).isNull();
+ }
+
+ @Test
+ public void setCopyMinScore() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyMinScore).isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyMinScore = true;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyMinScore).isTrue();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyMinScore).isTrue();
+ }
+
+ @Test
+ public void unsetCopyMinScore() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setCopyMinScore(true);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyMinScore).isTrue();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyMinScore = false;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyMinScore).isNull();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyMinScore).isNull();
+ }
+
+ @Test
+ public void setCopyMaxScore() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyMaxScore).isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyMaxScore = true;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyMaxScore).isTrue();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyMaxScore).isTrue();
+ }
+
+ @Test
+ public void unsetCopyMaxScore() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setCopyMaxScore(true);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyMaxScore).isTrue();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyMaxScore = false;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyMaxScore).isNull();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyMaxScore).isNull();
+ }
+
+ @Test
+ public void setCopyAllScoresIfNoChange() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setCopyAllScoresIfNoChange(false);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoChange)
+ .isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyAllScoresIfNoChange = true;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyAllScoresIfNoChange).isTrue();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoChange)
+ .isTrue();
+ }
+
+ @Test
+ public void unsetCopyAllScoresIfNoChange() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoChange)
+ .isTrue();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyAllScoresIfNoChange = false;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyAllScoresIfNoChange).isNull();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoChange)
+ .isNull();
+ }
+
+ @Test
+ public void setCopyAllScoresIfNoCodeChange() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoCodeChange)
+ .isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyAllScoresIfNoCodeChange = true;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyAllScoresIfNoCodeChange).isTrue();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoCodeChange)
+ .isTrue();
+ }
+
+ @Test
+ public void unsetCopyAllScoresIfNoCodeChange() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setCopyAllScoresIfNoCodeChange(true);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoCodeChange)
+ .isTrue();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyAllScoresIfNoCodeChange = false;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyAllScoresIfNoCodeChange).isNull();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresIfNoCodeChange)
+ .isNull();
+ }
+
+ @Test
+ public void setCopyAllScoresOnTrivialRebase() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresOnTrivialRebase)
+ .isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyAllScoresOnTrivialRebase = true;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyAllScoresOnTrivialRebase).isTrue();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresOnTrivialRebase)
+ .isTrue();
+ }
+
+ @Test
+ public void unsetCopyAllScoresOnTrivialRebase() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setCopyAllScoresOnTrivialRebase(true);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresOnTrivialRebase)
+ .isTrue();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyAllScoresOnTrivialRebase = false;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyAllScoresOnTrivialRebase).isNull();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().copyAllScoresOnTrivialRebase)
+ .isNull();
+ }
+
+ @Test
+ public void setCopyAllScoresOnMergeFirstParentUpdate() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(
+ gApi.projects()
+ .name(project.get())
+ .label("foo")
+ .get()
+ .copyAllScoresOnMergeFirstParentUpdate)
+ .isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyAllScoresOnMergeFirstParentUpdate = true;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyAllScoresOnMergeFirstParentUpdate).isTrue();
+
+ assertThat(
+ gApi.projects()
+ .name(project.get())
+ .label("foo")
+ .get()
+ .copyAllScoresOnMergeFirstParentUpdate)
+ .isTrue();
+ }
+
+ @Test
+ public void unsetCopyAllScoresOnMergeFirstParentUpdate() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setCopyAllScoresOnMergeFirstParentUpdate(true);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+ assertThat(
+ gApi.projects()
+ .name(project.get())
+ .label("foo")
+ .get()
+ .copyAllScoresOnMergeFirstParentUpdate)
+ .isTrue();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.copyAllScoresOnMergeFirstParentUpdate = false;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.copyAllScoresOnMergeFirstParentUpdate).isNull();
+
+ assertThat(
+ gApi.projects()
+ .name(project.get())
+ .label("foo")
+ .get()
+ .copyAllScoresOnMergeFirstParentUpdate)
+ .isNull();
+ }
+
+ @Test
+ public void setAllowPostSubmit() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setAllowPostSubmit(false);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+ assertThat(gApi.projects().name(project.get()).label("foo").get().allowPostSubmit).isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.allowPostSubmit = true;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.allowPostSubmit).isTrue();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().allowPostSubmit).isTrue();
+ }
+
+ @Test
+ public void unsetAllowPostSubmit() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).label("foo").get().allowPostSubmit).isTrue();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.allowPostSubmit = false;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.allowPostSubmit).isNull();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().allowPostSubmit).isNull();
+ }
+
+ @Test
+ public void setIgnoreSelfApproval() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ assertThat(gApi.projects().name(project.get()).label("foo").get().ignoreSelfApproval).isNull();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.ignoreSelfApproval = true;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.ignoreSelfApproval).isTrue();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().ignoreSelfApproval).isTrue();
+ }
+
+ @Test
+ public void unsetIgnoreSelfApproval() throws Exception {
+ configLabel("foo", LabelFunction.NO_OP);
+ try (ProjectConfigUpdate u = updateProject(project)) {
+ LabelType labelType = u.getConfig().getLabelSections().get("foo");
+ labelType.setIgnoreSelfApproval(true);
+ u.getConfig().getLabelSections().put(labelType.getName(), labelType);
+ u.save();
+ }
+ assertThat(gApi.projects().name(project.get()).label("foo").get().ignoreSelfApproval).isTrue();
+
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.ignoreSelfApproval = false;
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects().name(project.get()).label("foo").update(input);
+ assertThat(updatedLabel.ignoreSelfApproval).isNull();
+
+ assertThat(gApi.projects().name(project.get()).label("foo").get().ignoreSelfApproval).isNull();
+ }
+
+ @Test
+ public void noOpUpdate() throws Exception {
+ RevCommit refsMetaConfigHead =
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG);
+
+ LabelDefinitionInfo updatedLabel =
+ gApi.projects()
+ .name(allProjects.get())
+ .label("Code-Review")
+ .update(new LabelDefinitionInput());
+ LabelAssert.assertCodeReviewLabel(updatedLabel);
+
+ LabelAssert.assertCodeReviewLabel(
+ gApi.projects().name(allProjects.get()).label("Code-Review").get());
+
+ assertThat(projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG))
+ .isEqualTo(refsMetaConfigHead);
+ }
+
+ @Test
+ public void defaultCommitMessage() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.function = LabelFunction.NO_OP.getFunctionName();
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo("Update label");
+ }
+
+ @Test
+ public void withCommitMessage() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.function = LabelFunction.NO_OP.getFunctionName();
+ input.commitMessage = "Set NoOp function";
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo(input.commitMessage);
+ }
+
+ @Test
+ public void commitMessageIsTrimmed() throws Exception {
+ LabelDefinitionInput input = new LabelDefinitionInput();
+ input.function = LabelFunction.NO_OP.getFunctionName();
+ input.commitMessage = " Set NoOp function ";
+ gApi.projects().name(allProjects.get()).label("Code-Review").update(input);
+ assertThat(
+ projectOperations.project(allProjects).getHead(RefNames.REFS_CONFIG).getShortMessage())
+ .isEqualTo("Set NoOp function");
+ }
+}
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index b94a709..fd6c512 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -55,6 +55,7 @@
"//java/com/google/gerrit/server/account/externalids/testing",
"//java/com/google/gerrit/server/cache/serialize",
"//java/com/google/gerrit/server/cache/testing",
+ "//java/com/google/gerrit/server/git/receive:ref_cache",
"//java/com/google/gerrit/server/ioutil",
"//java/com/google/gerrit/server/logging",
"//java/com/google/gerrit/server/project/testing:project-test-util",
diff --git a/javatests/com/google/gerrit/server/git/GroupCollectorTest.java b/javatests/com/google/gerrit/server/git/GroupCollectorTest.java
index 6175385..63f83b0 100644
--- a/javatests/com/google/gerrit/server/git/GroupCollectorTest.java
+++ b/javatests/com/google/gerrit/server/git/GroupCollectorTest.java
@@ -21,10 +21,13 @@
import com.google.common.collect.SortedSetMultimap;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.server.git.receive.ReceivePackRefCache;
+import java.io.IOException;
import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -44,8 +47,7 @@
RevCommit branchTip = tr.commit().create();
RevCommit a = tr.commit().parent(branchTip).create();
- SortedSetMultimap<ObjectId, String> groups =
- collectGroups(newWalk(a, branchTip), patchSets(), groups());
+ SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(a, branchTip), groups());
assertThat(groups).containsEntry(a, a.name());
}
@@ -56,8 +58,7 @@
RevCommit a = tr.commit().parent(branchTip).create();
RevCommit b = tr.commit().parent(a).create();
- SortedSetMultimap<ObjectId, String> groups =
- collectGroups(newWalk(b, branchTip), patchSets(), groups());
+ SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(b, branchTip), groups());
assertThat(groups).containsEntry(a, a.name());
assertThat(groups).containsEntry(b, a.name());
@@ -67,12 +68,12 @@
public void commitWhoseParentIsExistingPatchSetGetsParentsGroup() throws Exception {
RevCommit branchTip = tr.commit().create();
RevCommit a = tr.commit().parent(branchTip).create();
+ createRef(psId(1, 1), a, tr);
RevCommit b = tr.commit().parent(a).create();
String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
SortedSetMultimap<ObjectId, String> groups =
- collectGroups(
- newWalk(b, branchTip), patchSets().put(a, psId(1, 1)), groups().put(psId(1, 1), group));
+ collectGroups(newWalk(b, branchTip), groups().put(psId(1, 1), group));
assertThat(groups).containsEntry(a, group);
assertThat(groups).containsEntry(b, group);
@@ -84,8 +85,7 @@
RevCommit a = tr.commit().parent(branchTip).create();
RevCommit b = tr.commit().parent(a).create();
- SortedSetMultimap<ObjectId, String> groups =
- collectGroups(newWalk(b, branchTip), patchSets().put(a, psId(1, 1)), groups());
+ SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(b, branchTip), groups());
assertThat(groups).containsEntry(a, a.name());
assertThat(groups).containsEntry(b, a.name());
@@ -98,8 +98,7 @@
RevCommit b = tr.commit().parent(branchTip).create();
RevCommit m = tr.commit().parent(a).parent(b).create();
- SortedSetMultimap<ObjectId, String> groups =
- collectGroups(newWalk(m, branchTip), patchSets(), groups());
+ SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(m, branchTip), groups());
assertThat(groups).containsEntry(a, a.name());
assertThat(groups).containsEntry(b, a.name());
@@ -110,13 +109,13 @@
public void mergeCommitWhereOneParentHasExistingGroup() throws Exception {
RevCommit branchTip = tr.commit().create();
RevCommit a = tr.commit().parent(branchTip).create();
+ createRef(psId(1, 1), a, tr);
RevCommit b = tr.commit().parent(branchTip).create();
RevCommit m = tr.commit().parent(a).parent(b).create();
String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
SortedSetMultimap<ObjectId, String> groups =
- collectGroups(
- newWalk(m, branchTip), patchSets().put(b, psId(1, 1)), groups().put(psId(1, 1), group));
+ collectGroups(newWalk(m, branchTip), groups().put(psId(1, 1), group));
// Merge commit and other parent get the existing group.
assertThat(groups).containsEntry(a, group);
@@ -128,16 +127,16 @@
public void mergeCommitWhereBothParentsHaveDifferentGroups() throws Exception {
RevCommit branchTip = tr.commit().create();
RevCommit a = tr.commit().parent(branchTip).create();
+ createRef(psId(1, 1), a, tr);
RevCommit b = tr.commit().parent(branchTip).create();
+ createRef(psId(2, 1), b, tr);
RevCommit m = tr.commit().parent(a).parent(b).create();
String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
String group2 = "1234567812345678123456781234567812345678";
SortedSetMultimap<ObjectId, String> groups =
collectGroups(
- newWalk(m, branchTip),
- patchSets().put(a, psId(1, 1)).put(b, psId(2, 1)),
- groups().put(psId(1, 1), group1).put(psId(2, 1), group2));
+ newWalk(m, branchTip), groups().put(psId(1, 1), group1).put(psId(2, 1), group2));
assertThat(groups).containsEntry(a, group1);
assertThat(groups).containsEntry(b, group2);
@@ -149,7 +148,9 @@
public void mergeCommitMergesGroupsFromParent() throws Exception {
RevCommit branchTip = tr.commit().create();
RevCommit a = tr.commit().parent(branchTip).create();
+ createRef(psId(1, 1), a, tr);
RevCommit b = tr.commit().parent(branchTip).create();
+ createRef(psId(2, 1), b, tr);
RevCommit m = tr.commit().parent(a).parent(b).create();
String group1 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
@@ -158,7 +159,6 @@
SortedSetMultimap<ObjectId, String> groups =
collectGroups(
newWalk(m, branchTip),
- patchSets().put(a, psId(1, 1)).put(b, psId(2, 1)),
groups().put(psId(1, 1), group1).put(psId(2, 1), group2a).put(psId(2, 1), group2b));
assertThat(groups).containsEntry(a, group1);
@@ -171,12 +171,12 @@
public void mergeCommitWithOneUninterestingParentAndOtherParentIsExisting() throws Exception {
RevCommit branchTip = tr.commit().create();
RevCommit a = tr.commit().parent(branchTip).create();
+ createRef(psId(1, 1), a, tr);
RevCommit m = tr.commit().parent(branchTip).parent(a).create();
String group = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
SortedSetMultimap<ObjectId, String> groups =
- collectGroups(
- newWalk(m, branchTip), patchSets().put(a, psId(1, 1)), groups().put(psId(1, 1), group));
+ collectGroups(newWalk(m, branchTip), groups().put(psId(1, 1), group));
assertThat(groups).containsEntry(a, group);
assertThat(groups).containsEntry(m, group);
@@ -188,8 +188,7 @@
RevCommit a = tr.commit().parent(branchTip).create();
RevCommit m = tr.commit().parent(branchTip).parent(a).create();
- SortedSetMultimap<ObjectId, String> groups =
- collectGroups(newWalk(m, branchTip), patchSets(), groups());
+ SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(m, branchTip), groups());
assertThat(groups).containsEntry(a, a.name());
assertThat(groups).containsEntry(m, a.name());
@@ -204,8 +203,7 @@
RevCommit m1 = tr.commit().parent(b).parent(c).create();
RevCommit m2 = tr.commit().parent(a).parent(m1).create();
- SortedSetMultimap<ObjectId, String> groups =
- collectGroups(newWalk(m2, branchTip), patchSets(), groups());
+ SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(m2, branchTip), groups());
assertThat(groups).containsEntry(a, a.name());
assertThat(groups).containsEntry(b, a.name());
@@ -223,8 +221,7 @@
assertThat(m.getParentCount()).isEqualTo(2);
assertThat(m.getParent(0)).isEqualTo(m.getParent(1));
- SortedSetMultimap<ObjectId, String> groups =
- collectGroups(newWalk(m, branchTip), patchSets(), groups());
+ SortedSetMultimap<ObjectId, String> groups = collectGroups(newWalk(m, branchTip), groups());
assertThat(groups).containsEntry(a, a.name());
assertThat(groups).containsEntry(m, a.name());
@@ -234,7 +231,9 @@
public void mergeCommitWithOneNewParentAndTwoExistingPatchSets() throws Exception {
RevCommit branchTip = tr.commit().create();
RevCommit a = tr.commit().parent(branchTip).create();
+ createRef(psId(1, 1), a, tr);
RevCommit b = tr.commit().parent(branchTip).create();
+ createRef(psId(2, 1), b, tr);
RevCommit c = tr.commit().parent(b).create();
RevCommit m = tr.commit().parent(a).parent(c).create();
@@ -242,9 +241,7 @@
String group2 = "1234567812345678123456781234567812345678";
SortedSetMultimap<ObjectId, String> groups =
collectGroups(
- newWalk(m, branchTip),
- patchSets().put(a, psId(1, 1)).put(b, psId(2, 1)),
- groups().put(psId(1, 1), group1).put(psId(2, 1), group2));
+ newWalk(m, branchTip), groups().put(psId(1, 1), group1).put(psId(2, 1), group2));
assertThat(groups).containsEntry(a, group1);
assertThat(groups).containsEntry(b, group2);
@@ -264,16 +261,7 @@
rw.markStart(rw.parseCommit(d));
// Schema upgrade case: all commits are existing patch sets, but none have
// groups assigned yet.
- SortedSetMultimap<ObjectId, String> groups =
- collectGroups(
- rw,
- patchSets()
- .put(branchTip, psId(1, 1))
- .put(a, psId(2, 1))
- .put(b, psId(3, 1))
- .put(c, psId(4, 1))
- .put(d, psId(5, 1)),
- groups());
+ SortedSetMultimap<ObjectId, String> groups = collectGroups(rw, groups());
assertThat(groups).containsEntry(a, a.name());
assertThat(groups).containsEntry(b, a.name());
@@ -287,6 +275,13 @@
return PatchSet.id(Change.id(c), p);
}
+ private static void createRef(PatchSet.Id psId, ObjectId id, TestRepository<?> tr)
+ throws IOException {
+ RefUpdate ru = tr.getRepository().updateRef(psId.toRefName());
+ ru.setNewObjectId(id);
+ assertThat(ru.update()).isEqualTo(RefUpdate.Result.NEW);
+ }
+
private RevWalk newWalk(ObjectId start, ObjectId branchTip) throws Exception {
// Match RevWalk conditions from ReceiveCommits.
RevWalk rw = new RevWalk(tr.getRepository());
@@ -297,12 +292,12 @@
return rw;
}
- private static SortedSetMultimap<ObjectId, String> collectGroups(
- RevWalk rw,
- ImmutableListMultimap.Builder<ObjectId, PatchSet.Id> patchSetsBySha,
- ImmutableListMultimap.Builder<PatchSet.Id, String> groupLookup)
- throws Exception {
- GroupCollector gc = new GroupCollector(patchSetsBySha.build(), groupLookup.build());
+ private SortedSetMultimap<ObjectId, String> collectGroups(
+ RevWalk rw, ImmutableListMultimap.Builder<PatchSet.Id, String> groupLookup) throws Exception {
+ ImmutableListMultimap<PatchSet.Id, String> groups = groupLookup.build();
+ GroupCollector gc =
+ new GroupCollector(
+ ReceivePackRefCache.noCache(tr.getRepository().getRefDatabase()), (s) -> groups.get(s));
RevCommit c;
while ((c = rw.next()) != null) {
gc.visit(c);
@@ -310,12 +305,6 @@
return gc.getGroups();
}
- // Helper methods for constructing various map arguments, to avoid lots of
- // type specifications.
- private static ImmutableListMultimap.Builder<ObjectId, PatchSet.Id> patchSets() {
- return ImmutableListMultimap.builder();
- }
-
private static ImmutableListMultimap.Builder<PatchSet.Id, String> groups() {
return ImmutableListMultimap.builder();
}
diff --git a/javatests/com/google/gerrit/server/git/receive/ReceivePackRefCacheTest.java b/javatests/com/google/gerrit/server/git/receive/ReceivePackRefCacheTest.java
new file mode 100644
index 0000000..698acd8
--- /dev/null
+++ b/javatests/com/google/gerrit/server/git/receive/ReceivePackRefCacheTest.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.receive;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.RefNames;
+import java.util.Map;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectIdRef;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefDatabase;
+import org.junit.Test;
+
+/** Tests for {@link ReceivePackRefCache}. */
+public class ReceivePackRefCacheTest {
+
+ @Test
+ public void noCache_prefixDelegatesToRefDb() throws Exception {
+ Ref ref = newRef("refs/changes/01/1/1", "badc0feebadc0feebadc0feebadc0feebadc0fee");
+ RefDatabase mockRefDb = mock(RefDatabase.class);
+ ReceivePackRefCache cache = ReceivePackRefCache.noCache(mockRefDb);
+ when(mockRefDb.getRefsByPrefix(RefNames.REFS_HEADS)).thenReturn(ImmutableList.of(ref));
+
+ assertThat(cache.byPrefix(RefNames.REFS_HEADS)).containsExactly(ref);
+ verify(mockRefDb).getRefsByPrefix(RefNames.REFS_HEADS);
+ verifyNoMoreInteractions(mockRefDb);
+ }
+
+ @Test
+ public void noCache_exactRefDelegatesToRefDb() throws Exception {
+ Ref ref = newRef("refs/changes/01/1/1", "badc0feebadc0feebadc0feebadc0feebadc0fee");
+ RefDatabase mockRefDb = mock(RefDatabase.class);
+ ReceivePackRefCache cache = ReceivePackRefCache.noCache(mockRefDb);
+ when(mockRefDb.exactRef("refs/heads/master")).thenReturn(ref);
+
+ assertThat(cache.exactRef("refs/heads/master")).isEqualTo(ref);
+ verify(mockRefDb).exactRef("refs/heads/master");
+ verifyNoMoreInteractions(mockRefDb);
+ }
+
+ @Test
+ public void noCache_tipsFromObjectIdDelegatesToRefDbAndFiltersByPrefix() throws Exception {
+ Ref refBla = newRef("refs/bla", "badc0feebadc0feebadc0feebadc0feebadc0fee");
+ Ref refheads = newRef(RefNames.REFS_HEADS, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+
+ RefDatabase mockRefDb = mock(RefDatabase.class);
+ ReceivePackRefCache cache = ReceivePackRefCache.noCache(mockRefDb);
+ when(mockRefDb.getTipsWithSha1(ObjectId.zeroId()))
+ .thenReturn(ImmutableSet.of(refBla, refheads));
+
+ assertThat(cache.tipsFromObjectId(ObjectId.zeroId(), RefNames.REFS_HEADS))
+ .containsExactly(refheads);
+ verify(mockRefDb).getTipsWithSha1(ObjectId.zeroId());
+ verifyNoMoreInteractions(mockRefDb);
+ }
+
+ @Test
+ public void advertisedRefs_prefixScans() throws Exception {
+ Ref refBla =
+ new ObjectIdRef.Unpeeled(
+ Ref.Storage.NEW,
+ "refs/bla/1",
+ ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee"),
+ 1);
+ ReceivePackRefCache cache =
+ ReceivePackRefCache.withAdvertisedRefs(() -> ImmutableMap.of(refBla.getName(), refBla));
+
+ assertThat(cache.byPrefix("refs/bla")).containsExactly(refBla);
+ }
+
+ @Test
+ public void advertisedRefs_prefixScansChangeId() throws Exception {
+ Map<String, Ref> refs = setupTwoChanges();
+ ReceivePackRefCache cache = ReceivePackRefCache.withAdvertisedRefs(() -> refs);
+
+ assertThat(cache.byPrefix(RefNames.changeRefPrefix(Change.id(1))))
+ .containsExactly(refs.get("refs/changes/01/1/1"));
+ }
+
+ @Test
+ public void advertisedRefs_exactRef() throws Exception {
+ Map<String, Ref> refs = setupTwoChanges();
+ ReceivePackRefCache cache = ReceivePackRefCache.withAdvertisedRefs(() -> refs);
+
+ assertThat(cache.exactRef("refs/changes/01/1/1")).isEqualTo(refs.get("refs/changes/01/1/1"));
+ }
+
+ @Test
+ public void advertisedRefs_tipsFromObjectIdWithNoPrefix() throws Exception {
+ Map<String, Ref> refs = setupTwoChanges();
+ ReceivePackRefCache cache = ReceivePackRefCache.withAdvertisedRefs(() -> refs);
+
+ assertThat(
+ cache.tipsFromObjectId(
+ ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee"), null))
+ .containsExactly(refs.get("refs/changes/01/1/1"));
+ }
+
+ @Test
+ public void advertisedRefs_tipsFromObjectIdWithPrefix() throws Exception {
+ Map<String, Ref> refs = setupTwoChanges();
+ ReceivePackRefCache cache = ReceivePackRefCache.withAdvertisedRefs(() -> refs);
+
+ assertThat(
+ cache.tipsFromObjectId(
+ ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee"), "/refs/some"))
+ .isEmpty();
+ }
+
+ private static Ref newRef(String name, String sha1) {
+ return new ObjectIdRef.Unpeeled(Ref.Storage.NEW, name, ObjectId.fromString(sha1), 1);
+ }
+
+ private Map<String, Ref> setupTwoChanges() {
+ Ref ref1 = newRef("refs/changes/01/1/1", "badc0feebadc0feebadc0feebadc0feebadc0fee");
+ Ref ref2 = newRef("refs/changes/02/2/1", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
+ return ImmutableMap.of(ref1.getName(), ref1, ref2.getName(), ref2);
+ }
+}
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 767e6bf..92ce310 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 767e6bfc028f438b4a1234ad6179aaba78a8b11e
+Subproject commit 92ce310ecf717133601b9e824c38bc5e5eafecba
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
index 86f8aaf..e60909c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -89,6 +89,7 @@
teardown(() => {
sandbox.restore();
+ Gerrit._testOnly_resetPlugins();
});
suite('by default', () => {
@@ -141,7 +142,7 @@
new URL('test/plugin.html?' + Math.random(),
window.location.href).toString());
sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
- Gerrit._setPluginsPending([]);
+ Gerrit._loadPlugins([]);
element = createElement();
});
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index fe09869..caa38d9 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -725,7 +725,7 @@
},
'0.1',
'http://some/plugins/url.html');
- Gerrit._setPluginsCount(0);
+ Gerrit._loadPlugins([]);
flush(() => {
assert.strictEqual(hookEl.plugin, plugin);
assert.strictEqual(hookEl.change, element.change);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index f9c7798..f7d1a18 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -81,7 +81,7 @@
});
element = fixture('basic');
sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve());
- Gerrit._setPluginsCount(0);
+ Gerrit._loadPlugins([]);
});
teardown(done => {
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index 6639b5a..ba4a1a9 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -118,7 +118,7 @@
_computeItems(messages, reviewerUpdates) {
// Polymer 2: check for undefined
if ([messages, reviewerUpdates].some(arg => arg === undefined)) {
- return undefined;
+ return [];
}
messages = messages || [];
@@ -155,15 +155,19 @@
},
_expandedChanged(exp) {
- for (let i = 0; i < this._processedMessages.length; i++) {
- this._processedMessages[i].expanded = exp;
+ if (this._processedMessages) {
+ for (let i = 0; i < this._processedMessages.length; i++) {
+ this._processedMessages[i].expanded = exp;
+ }
}
// _visibleMessages is a subarray of _processedMessages
// _processedMessages contains all items from _visibleMessages
// At this point all _visibleMessages.expanded values are set,
// and notifyPath must be used to notify Polymer about changes.
- for (let i = 0; i < this._visibleMessages.length; i++) {
- this.notifyPath(`_visibleMessages.${i}.expanded`);
+ if (this._visibleMessages) {
+ for (let i = 0; i < this._visibleMessages.length; i++) {
+ this.notifyPath(`_visibleMessages.${i}.expanded`);
+ }
}
},
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 30d322e..6306933 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -41,14 +41,6 @@
DETECTED: 'Extension detected',
};
- // Page visibility related constants.
- const PAGE_VISIBILITY = {
- TYPE: 'lifecycle',
- CATEGORY: 'Page Visibility',
- // Reported events - alphabetize below.
- STARTED_HIDDEN: 'hidden',
- };
-
// Navigation reporting constants.
const NAVIGATION = {
TYPE: 'nav-report',
@@ -107,8 +99,10 @@
const pending = [];
+ // Variables that hold context info in global scope
const loadedPlugins = [];
const detectedExtensions = [];
+ let reportRepoName = undefined;
const onError = function(oldOnError, msg, url, line, column, error) {
if (oldOnError) {
@@ -188,7 +182,13 @@
reporter(...args) {
const report = (this._isMetricsPluginLoaded() && !pending.length) ?
this.defaultReporter : this.cachingReporter;
- args.splice(4, 0, loadedPlugins, detectedExtensions);
+ const contextInfo = {
+ loadedPlugins,
+ detectedExtensions,
+ repoName: reportRepoName,
+ isInBackgroundTab: document.visibilityState === 'hidden',
+ };
+ args.splice(4, 0, contextInfo);
report.apply(this, args);
},
@@ -198,22 +198,27 @@
* @param {string} category
* @param {string} eventName
* @param {string|number} eventValue
- * @param {Array} plugins
- * @param {Array} extensions
+ * @param {Object} contextInfo
* @param {boolean|undefined} opt_noLog If true, the event will not be
* logged to the JS console.
*/
- defaultReporter(type, category, eventName, eventValue,
- loadedPlugins, detectedExtensions, opt_noLog) {
+ defaultReporter(type, category, eventName, eventValue, contextInfo,
+ opt_noLog) {
const detail = {
type,
category,
name: eventName,
value: eventValue,
};
- if (category === TIMING.CATEGORY_UI_LATENCY) {
- detail.loadedPlugins = loadedPlugins;
- detail.detectedExtensions = detectedExtensions;
+ if (category === TIMING.CATEGORY_UI_LATENCY && contextInfo) {
+ detail.loadedPlugins = contextInfo.loadedPlugins;
+ detail.detectedExtensions = contextInfo.detectedExtensions;
+ }
+ if (contextInfo && contextInfo.repoName) {
+ detail.repoName = contextInfo.repoName;
+ }
+ if (contextInfo && contextInfo.isInBackgroundTab !== undefined) {
+ detail.inBackgroundTab = contextInfo.isInBackgroundTab;
}
document.dispatchEvent(new CustomEvent(type, {detail}));
if (opt_noLog) { return; }
@@ -235,39 +240,34 @@
* @param {string} category
* @param {string} eventName
* @param {string|number} eventValue
- * @param {Array} plugins
- * @param {Array} extensions
+ * @param {Object} contextInfo
* @param {boolean|undefined} opt_noLog If true, the event will not be
* logged to the JS console.
*/
- cachingReporter(type, category, eventName, eventValue,
- plugins, extensions, opt_noLog) {
+ cachingReporter(type, category, eventName, eventValue, contextInfo,
+ opt_noLog) {
if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
console.error(eventValue && eventValue.error || eventName);
}
if (this._isMetricsPluginLoaded()) {
if (pending.length) {
for (const args of pending.splice(0)) {
- this.reporter(...args);
+ this.defaultReporter(...args);
}
}
- this.reporter(type, category, eventName, eventValue,
- plugins, extensions, opt_noLog);
+ this.defaultReporter(type, category, eventName, eventValue, contextInfo,
+ opt_noLog);
} else {
- pending.push([type, category, eventName, eventValue,
- plugins, extensions, opt_noLog]);
+ pending.push([type, category, eventName, eventValue, contextInfo,
+ opt_noLog]);
}
},
/**
* User-perceived app start time, should be reported when the app is ready.
*/
- appStarted(hidden) {
+ appStarted() {
this.timeEnd(TIMING.APP_STARTED);
- if (hidden) {
- this.reporter(PAGE_VISIBILITY.TYPE, PAGE_VISIBILITY.CATEGORY,
- PAGE_VISIBILITY.STARTED_HIDDEN);
- }
},
/**
@@ -296,6 +296,7 @@
this.time(TIMER.DIFF_VIEW_DISPLAYED);
this.time(TIMER.DIFF_VIEW_LOAD_FULL);
this.time(TIMER.FILE_LIST_DISPLAYED);
+ reportRepoName = undefined;
},
locationChanged(page) {
@@ -518,6 +519,10 @@
this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY,
'ErrorDialog: ' + message, {error: new Error(message)});
},
+
+ setRepoName(repoName) {
+ reportRepoName = repoName;
+ },
});
window.GrReporting = GrReporting;
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index c357979..04a3294 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -64,15 +64,11 @@
test('appStarted', () => {
sandbox.stub(element, 'now').returns(42);
- element.appStarted(true);
+ element.appStarted();
assert.isTrue(
element.reporter.calledWithExactly(
'timing-report', 'UI Latency', 'App Started', 42
));
- assert.isTrue(
- element.reporter.calledWithExactly(
- 'lifecycle', 'Page Visibility', 'hidden'
- ));
});
test('WebComponentsReady', () => {
@@ -289,9 +285,9 @@
// element.pluginLoaded('foo');
element.time('timeAction');
element.timeEnd('timeAction');
- assert.isTrue(element.defaultReporter.getCall(1).calledWith(
+ assert.isTrue(element.defaultReporter.getCall(1).calledWithMatch(
'timing-report', 'UI Latency', 'timeAction', 0,
- ['metrics-xyz1']
+ {loadedPlugins: ['metrics-xyz1']}
));
});
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 1421068..70bda79 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -1042,11 +1042,13 @@
},
_handleProjectDashboardRoute(data) {
+ const project = data.params[0];
this._setParams({
view: Gerrit.Nav.View.DASHBOARD,
- project: data.params[0],
+ project,
dashboard: decodeURIComponent(data.params[1]),
});
+ this.$.reporting.setRepoName(project);
},
_handleGroupInfoRoute(data) {
@@ -1121,27 +1123,33 @@
},
_handleRepoCommandsRoute(data) {
+ const repo = data.params[0];
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.COMMANDS,
- repo: data.params[0],
+ repo,
});
+ this.$.reporting.setRepoName(repo);
},
_handleRepoAccessRoute(data) {
+ const repo = data.params[0];
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.ACCESS,
- repo: data.params[0],
+ repo,
});
+ this.$.reporting.setRepoName(repo);
},
_handleRepoDashboardsRoute(data) {
+ const repo = data.params[0];
this._setParams({
view: Gerrit.Nav.View.REPO,
detail: Gerrit.Nav.RepoDetailView.DASHBOARDS,
- repo: data.params[0],
+ repo,
});
+ this.$.reporting.setRepoName(repo);
},
_handleBranchListOffsetRoute(data) {
@@ -1242,10 +1250,12 @@
},
_handleRepoRoute(data) {
+ const repo = data.params[0];
this._setParams({
view: Gerrit.Nav.View.REPO,
- repo: data.params[0],
+ repo,
});
+ this.$.reporting.setRepoName(repo);
},
_handlePluginListOffsetRoute(data) {
@@ -1307,6 +1317,7 @@
view: Gerrit.Nav.View.CHANGE,
};
+ this.$.reporting.setRepoName(params.project);
this._redirectOrNavigate(params);
},
@@ -1326,7 +1337,7 @@
params.leftSide = address.leftSide;
params.lineNum = address.lineNum;
}
-
+ this.$.reporting.setRepoName(params.project);
this._redirectOrNavigate(params);
},
@@ -1368,24 +1379,28 @@
_handleDiffEditRoute(ctx) {
// Parameter order is based on the regex group number matched.
+ const project = ctx.params[0];
this._redirectOrNavigate({
- project: ctx.params[0],
+ project,
changeNum: ctx.params[1],
patchNum: ctx.params[2],
path: ctx.params[3],
view: Gerrit.Nav.View.EDIT,
});
+ this.$.reporting.setRepoName(project);
},
_handleChangeEditRoute(ctx) {
// Parameter order is based on the regex group number matched.
+ const project = ctx.params[0];
this._redirectOrNavigate({
- project: ctx.params[0],
+ project,
changeNum: ctx.params[1],
patchNum: ctx.params[3],
view: Gerrit.Nav.View.CHANGE,
edit: true,
});
+ this.$.reporting.setRepoName(project);
},
/**
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index be0645e..0494b96 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -259,12 +259,14 @@
lastNotify: {left: 1, right: 1},
};
+ const rangesCache = new Map();
+
this._processPromise = util.makeCancelable(this._loadHLJS()
.then(() => {
return new Promise(resolve => {
const nextStep = () => {
this._processHandle = null;
- this._processNextLine(state);
+ this._processNextLine(state, rangesCache);
// Move to the next line in the section.
state.lineIndex++;
@@ -321,12 +323,21 @@
* Highlight.js emits and emit a list of text ranges and classes for the
* markers.
* @param {string} str The string of HTML.
+ * @param {Map<string, !Array<!Object>>} rangesCache A map for caching
+ * ranges for each string. A cache is read and written by this method.
+ * Since diff is mostly comparing same file on two sides, there is good rate
+ * of duplication at least for parts that are on left and right parts.
* @return {!Array<!Object>} The list of ranges.
*/
- _rangesFromString(str) {
+ _rangesFromString(str, rangesCache) {
+ const cached = rangesCache.get(str);
+ if (cached) return cached;
+
const div = document.createElement('div');
div.innerHTML = str;
- return this._rangesFromElement(div, 0);
+ const ranges = this._rangesFromElement(div, 0);
+ rangesCache.set(str, ranges);
+ return ranges;
},
_rangesFromElement(elem, offset) {
@@ -357,7 +368,7 @@
* lines).
* @param {!Object} state The processing state for the layer.
*/
- _processNextLine(state) {
+ _processNextLine(state, rangesCache) {
let baseLine;
let revisionLine;
@@ -386,7 +397,8 @@
baseLine = this._workaround(this._baseLanguage, baseLine);
result = this._hljs.highlight(this._baseLanguage, baseLine, true,
state.baseContext);
- this.push('_baseRanges', this._rangesFromString(result.value));
+ this.push('_baseRanges',
+ this._rangesFromString(result.value, rangesCache));
state.baseContext = result.top;
}
@@ -395,7 +407,8 @@
revisionLine = this._workaround(this._revisionLanguage, revisionLine);
result = this._hljs.highlight(this._revisionLanguage, revisionLine,
true, state.revisionContext);
- this.push('_revisionRanges', this._rangesFromString(result.value));
+ this.push('_revisionRanges',
+ this._rangesFromString(result.value, rangesCache));
state.revisionContext = result.top;
}
},
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index 472db21..4e3492f 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -388,10 +388,20 @@
'<span class="non-whtelisted-class">',
'<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>',
'</span>'].join('');
- const result = element._rangesFromString(str);
+ const result = element._rangesFromString(str, new Map());
assert.notEqual(result.length, 0);
});
+ test('_rangesFromString cache same syntax markers', () => {
+ sandbox.spy(element, '_rangesFromElement');
+ const str =
+ '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>';
+ const cacheMap = new Map();
+ element._rangesFromString(str, cacheMap);
+ element._rangesFromString(str, cacheMap);
+ assert.isTrue(element._rangesFromElement.calledOnce);
+ });
+
test('_isSectionDone', () => {
let state = {sectionIndex: 0, lineIndex: 0};
assert.isFalse(element._isSectionDone(state));
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 6e423cd..fc2ec20 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -118,7 +118,7 @@
},
ready() {
- this.$.reporting.appStarted(document.visibilityState === 'hidden');
+ this.$.reporting.appStarted();
this.$.router.start();
this.$.restAPI.getAccount().then(account => {
@@ -424,7 +424,8 @@
},
_computePluginScreenName({plugin, screen}) {
- return Gerrit._getPluginScreenName(plugin, screen);
+ if (!plugin || !screen) return '';
+ return `${plugin.getPluginName()}-screen-${screen}`;
},
_logWelcome() {
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
index 9cdcf76..2537a37 100644
--- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
@@ -39,7 +39,7 @@
let plugin;
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+ Gerrit._loadPlugins([]);
adminApi = plugin.admin();
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
index 994d666..b0ad585 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -67,7 +67,7 @@
replacementHook = plugin.registerCustomComponent(
'second', 'other-module', {replace: true});
// Mimic all plugins loaded.
- Gerrit._setPluginsPending([]);
+ Gerrit._loadPlugins([]);
flush(done);
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index bdc7652..21da106 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -27,33 +27,29 @@
},
},
- behaviors: [
- Gerrit.BaseUrlBehavior,
- ],
-
_configChanged(config) {
const plugins = config.plugin;
- const htmlPlugins = (plugins.html_resource_paths || [])
- .map(p => this._urlFor(p))
- .filter(p => !Gerrit._isPluginPreloaded(p));
+ const htmlPlugins = (plugins.html_resource_paths || []);
const jsPlugins =
- this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins)
- .map(p => this._urlFor(p))
- .filter(p => !Gerrit._isPluginPreloaded(p));
+ this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins);
+
const shouldLoadTheme = config.default_theme &&
!Gerrit._isPluginPreloaded('preloaded:gerrit-theme');
- const defaultTheme =
- shouldLoadTheme ? this._urlFor(config.default_theme) : null;
+ const themeToLoad =
+ shouldLoadTheme ? [config.default_theme] : [];
+
+ // Theme should be loaded first if has one to have better UX
const pluginsPending =
- [].concat(jsPlugins, htmlPlugins, defaultTheme || []);
- Gerrit._setPluginsPending(pluginsPending);
- if (defaultTheme) {
- // Make theme first to be first to load.
- // Load sync to work around rare theme loading race condition.
- this._importHtmlPlugins([defaultTheme], true);
+ themeToLoad.concat(jsPlugins, htmlPlugins);
+
+ const pluginOpts = {};
+
+ if (shouldLoadTheme) {
+ // Theme needs to be loaded synchronous.
+ pluginOpts[config.default_theme] = {sync: true};
}
- this._loadJsPlugins(jsPlugins);
- this._importHtmlPlugins(htmlPlugins);
+
+ Gerrit._loadPlugins(pluginsPending, pluginOpts);
},
/**
@@ -66,53 +62,5 @@
return !htmlPlugins.includes(counterpart);
});
},
-
- /**
- * @suppress {checkTypes}
- * States that it expects no more than 3 parameters, but that's not true.
- * @todo (beckysiegel) check Polymer annotations and submit change.
- * @param {Array} plugins
- * @param {boolean=} opt_sync
- */
- _importHtmlPlugins(plugins, opt_sync) {
- const async = !opt_sync;
- for (const url of plugins) {
- // onload (second param) needs to be a function. When null or undefined
- // were passed, plugins were not loaded correctly.
- (this.importHref || Polymer.importHref)(
- this._urlFor(url), () => {},
- Gerrit._pluginInstallError.bind(null, `${url} import error`),
- async);
- }
- },
-
- _loadJsPlugins(plugins) {
- for (const url of plugins) {
- this._createScriptTag(this._urlFor(url));
- }
- },
-
- _createScriptTag(url) {
- const el = document.createElement('script');
- el.defer = true;
- el.src = url;
- el.onerror = Gerrit._pluginInstallError.bind(null, `${url} load error`);
- return document.body.appendChild(el);
- },
-
- _urlFor(pathOrUrl) {
- if (!pathOrUrl) {
- return pathOrUrl;
- }
- if (pathOrUrl.startsWith('preloaded:') ||
- pathOrUrl.startsWith('http')) {
- // Plugins are loaded from another domain or preloaded.
- return pathOrUrl;
- }
- if (!pathOrUrl.startsWith('/')) {
- pathOrUrl = '/' + pathOrUrl;
- }
- return window.location.origin + this.getBaseUrl() + pathOrUrl;
- },
});
})();
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
index e577182..3a8e4d8 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -38,195 +38,57 @@
suite('gr-plugin-host tests', () => {
let element;
let sandbox;
- let url;
setup(() => {
element = fixture('basic');
sandbox = sinon.sandbox.create();
sandbox.stub(document.body, 'appendChild');
sandbox.stub(element, 'importHref');
- url = window.location.origin;
});
teardown(() => {
sandbox.restore();
});
- test('counts plugins', () => {
- sandbox.stub(Gerrit, '_setPluginsCount');
+ test('load plugins should be called', () => {
+ sandbox.stub(Gerrit, '_loadPlugins');
element.config = {
plugin: {
html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
js_resource_paths: ['plugins/42'],
},
};
- assert.isTrue(Gerrit._setPluginsCount.calledWith(3));
+ assert.isTrue(Gerrit._loadPlugins.calledOnce);
+ assert.isTrue(Gerrit._loadPlugins.calledWith([
+ 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
+ ], {}));
});
- test('imports relative html plugins from config', () => {
- sandbox.stub(Gerrit, '_pluginInstallError');
+ test('theme plugins should be loaded if enabled', () => {
+ sandbox.stub(Gerrit, '_loadPlugins');
element.config = {
- plugin: {html_resource_paths: ['foo/bar', 'baz']},
- };
- assert.equal(element.importHref.firstCall.args[0], url + '/foo/bar');
- assert.isTrue(element.importHref.firstCall.args[3]);
-
- assert.equal(element.importHref.secondCall.args[0], url + '/baz');
- assert.isTrue(element.importHref.secondCall.args[3]);
-
- assert.equal(Gerrit._pluginInstallError.callCount, 0);
- element.importHref.firstCall.args[2]();
- assert.equal(Gerrit._pluginInstallError.callCount, 1);
- element.importHref.secondCall.args[2]();
- assert.equal(Gerrit._pluginInstallError.callCount, 2);
- });
-
- test('imports relative html plugins from config with a base url', () => {
- sandbox.stub(Gerrit, '_pluginInstallError');
- sandbox.stub(element, 'getBaseUrl').returns('/the-base');
- element.config = {
- plugin: {html_resource_paths: ['foo/bar', 'baz']}};
- assert.equal(element.importHref.firstCall.args[0],
- url + '/the-base/foo/bar');
- assert.isTrue(element.importHref.firstCall.args[3]);
-
- assert.equal(element.importHref.secondCall.args[0],
- url + '/the-base/baz');
- assert.isTrue(element.importHref.secondCall.args[3]);
- assert.equal(Gerrit._pluginInstallError.callCount, 0);
- element.importHref.firstCall.args[2]();
- assert.equal(Gerrit._pluginInstallError.callCount, 1);
- element.importHref.secondCall.args[2]();
- assert.equal(Gerrit._pluginInstallError.callCount, 2);
- });
-
- test('importHref is not called with null callback functions', () => {
- const plugins = ['path/to/plugin'];
- element._importHtmlPlugins(plugins);
- assert.isTrue(element.importHref.calledOnce);
- assert.isFunction(element.importHref.lastCall.args[1]);
- assert.isFunction(element.importHref.lastCall.args[2]);
- });
-
- test('imports absolute html plugins from config', () => {
- sandbox.stub(Gerrit, '_pluginInstallError');
- element.config = {
+ default_theme: 'gerrit-theme.html',
plugin: {
- html_resource_paths: [
- 'http://example.com/foo/bar',
- 'https://example.com/baz',
- ],
+ html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
+ js_resource_paths: ['plugins/42'],
},
};
- assert.equal(element.importHref.firstCall.args[0],
- 'http://example.com/foo/bar');
- assert.isTrue(element.importHref.firstCall.args[3]);
-
- assert.equal(element.importHref.secondCall.args[0],
- 'https://example.com/baz');
- assert.isTrue(element.importHref.secondCall.args[3]);
- assert.equal(Gerrit._pluginInstallError.callCount, 0);
- element.importHref.firstCall.args[2]();
- assert.equal(Gerrit._pluginInstallError.callCount, 1);
- element.importHref.secondCall.args[2]();
- assert.equal(Gerrit._pluginInstallError.callCount, 2);
+ assert.isTrue(Gerrit._loadPlugins.calledOnce);
+ assert.isTrue(Gerrit._loadPlugins.calledWith([
+ 'gerrit-theme.html', 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
+ ], {'gerrit-theme.html': {sync: true}}));
});
- test('adds js plugins from config to the body', () => {
- element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
- assert.isTrue(document.body.appendChild.calledTwice);
- });
-
- test('imports relative js plugins from config', () => {
- sandbox.stub(element, '_createScriptTag');
- element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
- assert.isTrue(element._createScriptTag.calledWith(url + '/foo/bar'));
- assert.isTrue(element._createScriptTag.calledWith(url + '/baz'));
- });
-
- test('imports relative html plugins from config with a base url', () => {
- sandbox.stub(element, '_createScriptTag');
- sandbox.stub(element, 'getBaseUrl').returns('/the-base');
- element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
- assert.isTrue(element._createScriptTag.calledWith(
- url + '/the-base/foo/bar'));
- assert.isTrue(element._createScriptTag.calledWith(
- url + '/the-base/baz'));
- });
-
- test('imports absolute html plugins from config', () => {
- sandbox.stub(element, '_createScriptTag');
- element.config = {
- plugin: {
- js_resource_paths: [
- 'http://example.com/foo/bar',
- 'https://example.com/baz',
- ],
- },
- };
- assert.isTrue(element._createScriptTag.calledWith(
- 'http://example.com/foo/bar'));
- assert.isTrue(element._createScriptTag.calledWith(
- 'https://example.com/baz'));
- });
-
- test('default theme is loaded with html plugins', () => {
- sandbox.stub(Gerrit, '_pluginInstallError');
- element.config = {
- default_theme: '/oof',
- plugin: {
- html_resource_paths: ['some'],
- },
- };
- assert.equal(element.importHref.firstCall.args[0], url + '/oof');
- assert.isFalse(element.importHref.firstCall.args[3]);
-
- assert.equal(element.importHref.secondCall.args[0], url + '/some');
- assert.isTrue(element.importHref.secondCall.args[3]);
- assert.equal(Gerrit._pluginInstallError.callCount, 0);
- element.importHref.firstCall.args[2]();
- assert.equal(Gerrit._pluginInstallError.callCount, 1);
- element.importHref.secondCall.args[2]();
- assert.equal(Gerrit._pluginInstallError.callCount, 2);
- });
-
- test('default theme is loaded with html plugins', () => {
- sandbox.stub(Gerrit, '_setPluginsPending');
- element.config = {
- default_theme: '/oof',
- plugin: {},
- };
- assert.isTrue(Gerrit._setPluginsPending.calledWith([url + '/oof']));
- });
-
- test('skips default theme loading if preloaded', () => {
+ test('skip theme if preloaded', () => {
sandbox.stub(Gerrit, '_isPluginPreloaded')
.withArgs('preloaded:gerrit-theme').returns(true);
- sandbox.stub(Gerrit, '_setPluginsPending');
+ sandbox.stub(Gerrit, '_loadPlugins');
element.config = {
default_theme: '/oof',
plugin: {},
};
- assert.isFalse(element.importHref.calledWith(url + '/oof'));
- });
-
- test('skips preloaded plugins', () => {
- sandbox.stub(Gerrit, '_isPluginPreloaded')
- .withArgs(url + '/plugins/foo/bar').returns(true)
- .withArgs(url + '/plugins/42').returns(true);
- sandbox.stub(Gerrit, '_setPluginsCount');
- sandbox.stub(Gerrit, '_setPluginsPending');
- sandbox.stub(element, '_createScriptTag');
- element.config = {
- plugin: {
- html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
- js_resource_paths: ['plugins/42'],
- },
- };
- assert.isTrue(
- Gerrit._setPluginsPending.calledWith([url + '/plugins/baz']));
- assert.equal(element._createScriptTag.callCount, 0);
- assert.isTrue(element.importHref.calledWith(url + '/plugins/baz'));
+ assert.isTrue(Gerrit._loadPlugins.calledOnce);
+ assert.isTrue(Gerrit._loadPlugins.calledWith([], {}));
});
});
</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
index 7c7564b..0b32f8a 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
@@ -46,7 +46,7 @@
let plugin;
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+ Gerrit._loadPlugins([]);
repoApi = plugin.project();
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
index d34ca94..cbc2de6 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
@@ -48,7 +48,7 @@
let plugin;
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+ Gerrit._loadPlugins([]);
settingsApi = plugin.settings();
});
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
index d67a309..46bda6d 100644
--- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
@@ -46,7 +46,7 @@
let plugin;
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+ Gerrit._loadPlugins([]);
stylesApi = plugin.styles();
});
@@ -76,7 +76,7 @@
let plugin;
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
+ Gerrit._loadPlugins([]);
stylesApi = plugin.styles();
displayInlineStyle = stylesApi.css('display: inline');
displayNoneStyle = stylesApi.css('display: none');
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
index 82eb0f8..6332b91 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
@@ -67,7 +67,7 @@
stub('gr-custom-plugin-header', {
ready() { customHeader = this; },
});
- Gerrit._setPluginsPending([]);
+ Gerrit._loadPlugins([]);
});
test('sets logo and title', done => {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index 17e1b1c..2263113 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -52,10 +52,7 @@
value: () => [],
observer: '_resetCursorStops',
},
- _suggestionEls: {
- type: Array,
- observer: '_resetCursorIndex',
- },
+ _suggestionEls: Array,
},
behaviors: [
@@ -78,9 +75,9 @@
open() {
this.isHidden = false;
- this.refit();
this._resetCursorStops();
- this._resetCursorIndex();
+ // Refit should run after we call Polymer.flush inside _resetCursorStops
+ this.refit();
},
getCurrentText() {
@@ -162,10 +159,13 @@
_resetCursorStops() {
if (this.suggestions.length > 0) {
- Polymer.dom.flush();
- // Polymer2: querySelectorAll returns NodeList instead of Array.
- this._suggestionEls = Array.from(
- this.$.suggestions.querySelectorAll('li'));
+ if (!this.isHidden) {
+ Polymer.dom.flush();
+ // Polymer2: querySelectorAll returns NodeList instead of Array.
+ this._suggestionEls = Array.from(
+ this.$.suggestions.querySelectorAll('li'));
+ this._resetCursorIndex();
+ }
} else {
this._suggestionEls = [];
}
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
index 1c2afaa..3456c13 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -117,7 +117,7 @@
assert.strictEqual(element.style.backgroundImage, '');
// Emulate plugins loaded.
- Gerrit._setPluginsPending([]);
+ Gerrit._loadPlugins([]);
Promise.all([
element.$.restAPI.getConfig(),
@@ -155,7 +155,7 @@
assert.isFalse(element.hasAttribute('hidden'));
// Emulate plugins loaded.
- Gerrit._setPluginsPending([]);
+ Gerrit._loadPlugins([]);
return Promise.all([
element.$.restAPI.getConfig(),
@@ -197,7 +197,7 @@
_account_id: 123,
};
// Emulate plugins loaded.
- Gerrit._setPluginsPending([]);
+ Gerrit._loadPlugins([]);
return Promise.all([
element.$.restAPI.getConfig(),
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
index d613640..2a05050 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -139,7 +139,7 @@
<span
class$="itemAction [[_computeDisabledClass(link.id, disabledIds.*)]]"
data-id$="[[link.id]]"
- on-tap="_handleItemTap"
+ on-click="_handleItemTap"
hidden$="[[link.url]]"
tabindex="-1">[[link.name]]</span>
<a
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index ce6d043..50a20d1 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -138,10 +138,10 @@
e.preventDefault();
e.stopPropagation();
if (this.$.dropdown.opened) {
- // TODO(kaspern): This solution will not work in Shadow DOM, and
- // is not particularly robust in general. Find a better solution
- // when page.js has been abstracted away from components.
- const el = this.$.cursor.target.querySelector(':not([hidden])');
+ // TODO(milutin): This solution is not particularly robust in general.
+ // Since gr-tooltip-content click on shadow dom is not propagated down,
+ // we have to target `a` inside it.
+ const el = this.$.cursor.target.querySelector(':not([hidden]) a');
if (el) { el.click(); }
} else {
this._open();
@@ -175,8 +175,8 @@
*/
_open() {
this.$.dropdown.open();
+ this._resetCursorStops();
this.$.cursor.setCursorAtIndex(0);
- Polymer.dom.flush();
this.$.cursor.target.focus();
},
@@ -281,9 +281,8 @@
* Recompute the stops for the dropdown item cursor.
*/
_resetCursorStops() {
- if (this.items && this.items.length > 0) {
+ if (this.items && this.items.length > 0 && this.$.dropdown.opened) {
Polymer.dom.flush();
- // Polymer2: querySelectorAll returns NodeList instead of Array.
this._listElements = Array.from(
Polymer.dom(this.root).querySelectorAll('li'));
}
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index bf1c9fa..295f746 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -196,7 +196,7 @@
MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
assert.isTrue(element.$.dropdown.opened);
- const el = element.$.cursor.target.querySelector(':not([hidden])');
+ const el = element.$.cursor.target.querySelector(':not([hidden]) a');
const stub = sandbox.stub(el, 'click');
MockInteractions.pressAndReleaseKeyOn(element, 32); // Space
assert.isTrue(stub.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
index 7e7e927..3db7c63 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.js
@@ -19,6 +19,7 @@
'use strict';
const PRELOADED_PROTOCOL = 'preloaded:';
+ const PLUGIN_LOADING_TIMEOUT_MS = 10000;
let _restAPI;
function getRestAPI() {
@@ -28,6 +29,10 @@
return _restAPI;
}
+ function getBaseUrl() {
+ return Gerrit.BaseUrlBehavior.getBaseUrl();
+ }
+
/**
* Retrieves the name of the plugin base on the url.
* @param {string|URL} url
@@ -96,6 +101,9 @@
getPluginNameFromUrl,
send,
getRestAPI,
+ getBaseUrl,
+ PRELOADED_PROTOCOL,
+ PLUGIN_LOADING_TIMEOUT_MS,
// TEST only methods
testOnly_resetInternalState,
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
index 0131912..7332877 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -58,13 +58,14 @@
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
// Mimic all plugins loaded.
- Gerrit._setPluginsPending([]);
+ Gerrit._loadPlugins([]);
changeActions = plugin.changeActions();
element = fixture('basic');
});
teardown(() => {
changeActions = null;
+ Gerrit._testOnly_resetPlugins();
});
test('does not throw', ()=> {
@@ -85,11 +86,12 @@
'http://test.com/plugins/testplugin/static/test.js');
changeActions = plugin.changeActions();
// Mimic all plugins loaded.
- Gerrit._setPluginsPending([]);
+ Gerrit._loadPlugins([]);
});
teardown(() => {
changeActions = null;
+ Gerrit._testOnly_resetPlugins();
});
test('property existence', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
index a567700..03eb2e8 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.js
@@ -23,42 +23,12 @@
(function(window) {
'use strict';
- /**
- * Hash of loaded and installed plugins, name to Plugin object.
- */
- const _plugins = {};
-
- /**
- * Array of plugin URLs to be loaded, name to url.
- */
- let _pluginsPending = {};
-
- let _pluginsInstalled = [];
-
- let _pluginsPendingCount = -1;
-
- const UNKNOWN_PLUGIN = 'unknown';
- const PRELOADED_PROTOCOL = 'preloaded:';
-
- const PLUGIN_LOADING_TIMEOUT_MS = 10000;
-
- let _reporting;
- const getReporting = () => {
- if (!_reporting) {
- _reporting = document.createElement('gr-reporting');
- }
- return _reporting;
- };
-
// Import utils methods
const {
- getPluginNameFromUrl,
send,
getRestAPI,
} = window._apiUtils;
- const API_VERSION = '0.1';
-
/**
* Trigger the preinstalls for bundled plugins.
* This needs to happen before Gerrit as plugin bundle overrides the Gerrit.
@@ -72,9 +42,7 @@
window.Gerrit = window.Gerrit || {};
const Gerrit = window.Gerrit;
-
- let _resolveAllPluginsLoaded = null;
- let _allPluginsPromise = null;
+ Gerrit._pluginLoader = new PluginLoader();
Gerrit._endpoints = new GrPluginEndpoints();
@@ -85,20 +53,13 @@
const {
testOnly_resetInternalState,
} = window._apiUtils;
- Gerrit._testOnly_installPreloadedPlugins = installPreloadedPlugins;
+ Gerrit._testOnly_installPreloadedPlugins = (...args) => Gerrit._pluginLoader
+ .installPreloadedPlugins(...args);
Gerrit._testOnly_flushPreinstalls = flushPreinstalls;
Gerrit._testOnly_resetPlugins = () => {
- _allPluginsPromise = null;
- _pluginsInstalled = [];
- _pluginsPending = {};
- _pluginsPendingCount = -1;
- _reporting = null;
- _resolveAllPluginsLoaded = null;
testOnly_resetInternalState();
Gerrit._endpoints = new GrPluginEndpoints();
- for (const k of Object.keys(_plugins)) {
- delete _plugins[k];
- }
+ Gerrit._pluginLoader = new PluginLoader();
};
}
@@ -122,36 +83,7 @@
};
Gerrit.install = function(callback, opt_version, opt_src) {
- // HTML import polyfill adds __importElement pointing to the import tag.
- const script = document.currentScript &&
- (document.currentScript.__importElement || document.currentScript);
-
- let src = opt_src || (script && script.src);
- if (!src || src.startsWith('data:')) {
- src = script && script.baseURI;
- }
- const name = getPluginNameFromUrl(src);
-
- if (opt_version && opt_version !== API_VERSION) {
- Gerrit._pluginInstallError(`Plugin ${name} install error: only version ` +
- API_VERSION + ' is supported in PolyGerrit. ' + opt_version +
- ' was given.');
- return;
- }
-
- const existingPlugin = _plugins[name];
- const plugin = existingPlugin || new Plugin(src);
- try {
- callback(plugin);
- if (name) {
- _plugins[name] = plugin;
- }
- if (!existingPlugin) {
- Gerrit._pluginInstalled(src);
- }
- } catch (e) {
- Gerrit._pluginInstallError(`${e.name}: ${e.message}`);
- }
+ Gerrit._pluginLoader.install(callback, opt_version, opt_src);
};
Gerrit.getLoggedIn = function() {
@@ -195,96 +127,33 @@
};
Gerrit.awaitPluginsLoaded = function() {
- if (!_allPluginsPromise) {
- if (Gerrit._arePluginsLoaded()) {
- _allPluginsPromise = Promise.resolve();
- } else {
- let timeoutId;
- _allPluginsPromise =
- Promise.race([
- new Promise(resolve => _resolveAllPluginsLoaded = resolve),
- new Promise(resolve => timeoutId = setTimeout(
- Gerrit._pluginLoadingTimeout, PLUGIN_LOADING_TIMEOUT_MS)),
- ]).then(() => clearTimeout(timeoutId));
- }
- }
- return _allPluginsPromise;
+ return Gerrit._pluginLoader.awaitPluginsLoaded();
};
- Gerrit._pluginLoadingTimeout = function() {
- console.error(`Failed to load plugins: ${Object.keys(_pluginsPending)}`);
- Gerrit._setPluginsPending([]);
- };
+ // TODO(taoalpha): consider removing these proxy methods
+ // and using _pluginLoader directly
- Gerrit._setPluginsPending = function(plugins) {
- _pluginsPending = plugins.reduce((o, url) => {
- // TODO(viktard): Remove guard (@see Issue 8962)
- o[getPluginNameFromUrl(url) || UNKNOWN_PLUGIN] = url;
- return o;
- }, {});
- Gerrit._setPluginsCount(Object.keys(_pluginsPending).length);
- };
-
- Gerrit._setPluginsCount = function(count) {
- _pluginsPendingCount = count;
- if (Gerrit._arePluginsLoaded()) {
- getReporting().pluginsLoaded(_pluginsInstalled);
- if (_resolveAllPluginsLoaded) {
- _resolveAllPluginsLoaded();
- }
- }
- };
-
- Gerrit._pluginInstallError = function(message) {
- document.dispatchEvent(new CustomEvent('show-alert', {
- detail: {
- message: `Plugin install error: ${message}`,
- },
- }));
- console.info(`Plugin install error: ${message}`);
- Gerrit._setPluginsCount(_pluginsPendingCount - 1);
- };
-
- Gerrit._pluginInstalled = function(url) {
- const name = getPluginNameFromUrl(url) || UNKNOWN_PLUGIN;
- if (!_pluginsPending[name]) {
- console.warn(`Unexpected plugin ${name} installed from ${url}.`);
- } else {
- delete _pluginsPending[name];
- _pluginsInstalled.push(name);
- Gerrit._setPluginsCount(_pluginsPendingCount - 1);
- getReporting().pluginLoaded(name);
- console.log(`Plugin ${name} installed.`);
- }
+ Gerrit._loadPlugins = function(plugins, opt_option) {
+ Gerrit._pluginLoader.loadPlugins(plugins, opt_option);
};
Gerrit._arePluginsLoaded = function() {
- return _pluginsPendingCount === 0;
- };
-
- Gerrit._getPluginScreenName = function(pluginName, screenName) {
- return `${pluginName}-screen-${screenName}`;
+ return Gerrit._pluginLoader.arePluginsLoaded;
};
Gerrit._isPluginPreloaded = function(url) {
- const name = getPluginNameFromUrl(url);
- if (name && Gerrit._preloadedPlugins) {
- return name in Gerrit._preloadedPlugins;
- } else {
- return false;
- }
+ return Gerrit._pluginLoader.isPluginPreloaded(url);
};
- function installPreloadedPlugins() {
- if (!Gerrit._preloadedPlugins) { return; }
- for (const name in Gerrit._preloadedPlugins) {
- if (!Gerrit._preloadedPlugins.hasOwnProperty(name)) { continue; }
- const callback = Gerrit._preloadedPlugins[name];
- Gerrit.install(callback, API_VERSION, PRELOADED_PROTOCOL + name);
- }
- }
+ Gerrit._isPluginEnabled = function(pathOrUrl) {
+ return Gerrit._pluginLoader.isPluginEnabled(pathOrUrl);
+ };
+
+ Gerrit._isPluginLoaded = function(pathOrUrl) {
+ return Gerrit._pluginLoader.isPluginLoaded(pathOrUrl);
+ };
// Preloaded plugins should be installed after Gerrit.install() is set,
// since plugin preloader substitutes Gerrit.install() temporarily.
- installPreloadedPlugins();
+ Gerrit._pluginLoader.installPreloadedPlugins();
})(window);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
index 9a05454..e81b8aa 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
@@ -37,11 +37,11 @@
<script>
suite('gr-gerrit tests', () => {
let element;
- let plugin;
let sandbox;
let sendStub;
setup(() => {
+ this.clock = sinon.useFakeTimers();
sandbox = sinon.sandbox.create();
sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
stub('gr-rest-api-interface', {
@@ -53,136 +53,48 @@
},
});
element = fixture('basic');
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- Gerrit._setPluginsPending([]);
});
teardown(() => {
+ this.clock.restore();
sandbox.restore();
element._removeEventCallbacks();
- plugin = null;
+ Gerrit._testOnly_resetPlugins();
});
- test('reuse plugin for install calls', () => {
- let otherPlugin;
- Gerrit.install(p => { otherPlugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
- assert.strictEqual(plugin, otherPlugin);
- });
-
- test('flushes preinstalls if provided', () => {
- assert.doesNotThrow(() => {
- Gerrit._testOnly_flushPreinstalls();
+ suite('proxy methods', () => {
+ test('Gerrit._isPluginEnabled proxy to pluginLoader', () => {
+ const stubFn = sandbox.stub();
+ sandbox.stub(
+ Gerrit._pluginLoader,
+ 'isPluginEnabled',
+ (...args) => stubFn(...args)
+ );
+ Gerrit._isPluginEnabled('test_plugin');
+ assert.isTrue(stubFn.calledWith('test_plugin'));
});
- window.Gerrit.flushPreinstalls = sandbox.stub();
- Gerrit._testOnly_flushPreinstalls();
- assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce);
- delete window.Gerrit.flushPreinstalls;
- });
- test('versioning', () => {
- const callback = sandbox.spy();
- Gerrit.install(callback, '0.0pre-alpha');
- assert(callback.notCalled);
- });
-
- test('_setPluginsCount', done => {
- stub('gr-reporting', {
- pluginsLoaded() {
- done();
- },
+ test('Gerrit._isPluginLoaded proxy to pluginLoader', () => {
+ const stubFn = sandbox.stub();
+ sandbox.stub(
+ Gerrit._pluginLoader,
+ 'isPluginLoaded',
+ (...args) => stubFn(...args)
+ );
+ Gerrit._isPluginLoaded('test_plugin');
+ assert.isTrue(stubFn.calledWith('test_plugin'));
});
- Gerrit._setPluginsCount(0);
- });
- test('_arePluginsLoaded', () => {
- assert.isTrue(Gerrit._arePluginsLoaded());
- Gerrit._setPluginsCount(1);
- assert.isFalse(Gerrit._arePluginsLoaded());
- Gerrit._setPluginsCount(0);
- assert.isTrue(Gerrit._arePluginsLoaded());
- });
-
- test('_pluginInstalled', () => {
- const pluginsLoadedStub = sandbox.stub();
- stub('gr-reporting', {
- pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ test('Gerrit._isPluginPreloaded proxy to pluginLoader', () => {
+ const stubFn = sandbox.stub();
+ sandbox.stub(
+ Gerrit._pluginLoader,
+ 'isPluginPreloaded',
+ (...args) => stubFn(...args)
+ );
+ Gerrit._isPluginPreloaded('test_plugin');
+ assert.isTrue(stubFn.calledWith('test_plugin'));
});
- const plugins = [
- 'http://test.com/plugins/foo/static/test.js',
- 'http://test.com/plugins/bar/static/test.js',
- ];
- Gerrit._setPluginsPending(plugins);
- Gerrit._pluginInstalled(plugins[0]);
- Gerrit._pluginInstalled(plugins[1]);
- assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
- });
-
- test('install calls _pluginInstalled', () => {
- sandbox.stub(Gerrit, '_pluginInstalled');
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin/static/test.js');
-
- // testplugin has already been installed once (in setup).
- assert.isFalse(Gerrit._pluginInstalled.called);
-
- // testplugin2 plugin has not yet been installed.
- Gerrit.install(p => { plugin = p; }, '0.1',
- 'http://test.com/plugins/testplugin2/static/test.js');
- assert.isTrue(Gerrit._pluginInstalled.calledOnce);
- });
-
- test('plugin install errors mark plugins as loaded', () => {
- Gerrit._setPluginsCount(1);
- Gerrit.install(() => {}, '0.0pre-alpha');
- return Gerrit.awaitPluginsLoaded();
- });
-
- test('multiple ui plugins per java plugin', () => {
- const file1 = 'http://test.com/plugins/qaz/static/foo.nocache.js';
- const file2 = 'http://test.com/plugins/qaz/static/bar.js';
- Gerrit._setPluginsPending([file1, file2]);
- Gerrit.install(() => {}, '0.1', file1);
- Gerrit.install(() => {}, '0.1', file2);
- return Gerrit.awaitPluginsLoaded();
- });
-
- test('plugin install errors shows toasts', () => {
- const alertStub = sandbox.stub();
- document.addEventListener('show-alert', alertStub);
- Gerrit._setPluginsCount(1);
- Gerrit.install(() => {}, '0.0pre-alpha');
- return Gerrit.awaitPluginsLoaded().then(() => {
- assert.isTrue(alertStub.calledOnce);
- });
- });
-
- test('Gerrit._isPluginPreloaded', () => {
- Gerrit._preloadedPlugins = {baz: ()=>{}};
- assert.isFalse(Gerrit._isPluginPreloaded('plugins/foo/bar'));
- assert.isFalse(Gerrit._isPluginPreloaded('http://a.com/42'));
- assert.isTrue(Gerrit._isPluginPreloaded('preloaded:baz'));
- Gerrit._preloadedPlugins = null;
- });
-
- test('preloaded plugins are installed', () => {
- const installStub = sandbox.stub();
- Gerrit._preloadedPlugins = {foo: installStub};
- Gerrit._testOnly_installPreloadedPlugins();
- assert.isTrue(installStub.called);
- const pluginApi = installStub.lastCall.args[0];
- assert.strictEqual(pluginApi.getPluginName(), 'foo');
- });
-
- test('installing preloaded plugin', () => {
- let plugin;
- window.ASSETS_PATH = 'http://blips.com/chitz';
- Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
- assert.strictEqual(plugin.getPluginName(), 'foo');
- assert.strictEqual(plugin.url('/some/thing.html'),
- 'http://blips.com/chitz/plugins/foo/some/thing.html');
- delete window.ASSETS_PATH;
});
});
</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index 7fa2250..dc81545 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -31,6 +31,13 @@
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-js-api-interface">
+ <!--
+ Note: the order matters as files depend on each other.
+ 1. gr-api-utils will be used in multiple files below.
+ 2. gr-gerrit depends on gr-plugin-loader, gr-public-js-api and
+ also gr-plugin-endpoints
+ 3. gr-public-js-api depends on gr-plugin-rest-api
+ -->
<script src="gr-api-utils.js"></script>
<script src="gr-annotation-actions-context.js"></script>
<script src="gr-annotation-actions-js-api.js"></script>
@@ -41,5 +48,6 @@
<script src="gr-plugin-action-context.js"></script>
<script src="gr-plugin-rest-api.js"></script>
<script src="gr-public-js-api.js"></script>
+ <script src="gr-plugin-loader.js"></script>
<script src="gr-gerrit.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
index 330310f..ae12940 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -35,6 +35,7 @@
</test-fixture>
<script>
+ const {PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
suite('gr-js-api-interface tests', () => {
let element;
let plugin;
@@ -48,6 +49,7 @@
};
setup(() => {
+ this.clock = sinon.useFakeTimers();
sandbox = sinon.sandbox.create();
getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
@@ -64,10 +66,11 @@
errorStub = sandbox.stub(console, 'error');
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
- Gerrit._setPluginsPending([]);
+ Gerrit._loadPlugins([]);
});
teardown(() => {
+ this.clock.restore();
sandbox.restore();
element._removeEventCallbacks();
plugin = null;
@@ -194,12 +197,15 @@
revisions: {def: {_number: 2}, abc: {_number: 1}},
};
const spy = sandbox.spy();
- Gerrit._setPluginsCount(1);
+ Gerrit._loadPlugins(['plugins/test.html']);
plugin.on(element.EventType.SHOW_CHANGE, spy);
element.handleEvent(element.EventType.SHOW_CHANGE,
{change: testChange, patchNum: 1});
assert.isFalse(spy.called);
- Gerrit._setPluginsCount(0);
+
+ // Timeout on loading plugins
+ this.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
+
flush(() => {
assert.isTrue(spy.called);
done();
@@ -334,7 +340,6 @@
setup(() => {
sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r');
- Gerrit._setPluginsCount(1);
Gerrit.install(p => { baseUrlPlugin = p; }, '0.1',
'http://test.com/r/plugins/baseurlplugin/static/test.js');
});
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
index 229c020..6da117f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
@@ -42,7 +42,6 @@
setup(() => {
sandbox = sinon.sandbox.create();
- Gerrit._setPluginsCount(1);
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
instance = new GrPluginActionContext(plugin);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
new file mode 100644
index 0000000..04caf6b
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
@@ -0,0 +1,393 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function(window) {
+ 'use strict';
+
+ // Import utils methods
+ const {
+ PLUGIN_LOADING_TIMEOUT_MS,
+ PRELOADED_PROTOCOL,
+ getPluginNameFromUrl,
+ getBaseUrl,
+ } = window._apiUtils;
+
+ /**
+ * @enum {string}
+ */
+ const PluginState = {
+ /**
+ * State that indicates the plugin is pending to be loaded.
+ */
+ PENDING: 'PENDING',
+
+ /**
+ * State that indicates the plugin is already loaded.
+ */
+ LOADED: 'LOADED',
+
+ /**
+ * State that indicates the plugin is already loaded.
+ */
+ PRE_LOADED: 'PRE_LOADED',
+
+ /**
+ * State that indicates the plugin failed to load.
+ */
+ LOAD_FAILED: 'LOAD_FAILED',
+ };
+
+ // Prefix for any unrecognized plugin urls.
+ // Url should match following patterns:
+ // /plugins/PLUGINNAME/static/SCRIPTNAME.(html|js)
+ // /plugins/PLUGINNAME.(js|html)
+ const UNKNOWN_PLUGIN_PREFIX = '__$$__';
+
+ // Current API version for Plugin,
+ // plugins with incompatible version will not be laoded.
+ const API_VERSION = '0.1';
+
+ /**
+ * PluginLoader, responsible for:
+ *
+ * Loading all plugins and handling errors etc.
+ * Recording plugin state.
+ * Reporting on plugin loading status.
+ * Retrieve plugin.
+ * Check plugin status and if all plugins loaded.
+ */
+ class PluginLoader {
+ constructor() {
+ this._pluginListLoaded = false;
+
+ /** @type {Map<string,PluginLoader.PluginObject>} */
+ this._plugins = new Map();
+
+ this._reporting = null;
+
+ // Promise that resolves when all plugins loaded
+ this._loadingPromise = null;
+
+ // Resolver to resolve _loadingPromise once all plugins loaded
+ this._loadingResolver = null;
+ }
+
+ _getReporting() {
+ if (!this._reporting) {
+ this._reporting = document.createElement('gr-reporting');
+ }
+ return this._reporting;
+ }
+
+ /**
+ * Use the plugin name or use the full url if not recognized.
+ * @see gr-api-utils#getPluginNameFromUrl
+ * @param {string|URL} url
+ */
+ _getPluginKeyFromUrl(url) {
+ return getPluginNameFromUrl(url) ||
+ `${UNKNOWN_PLUGIN_PREFIX}${url}`;
+ }
+
+ /**
+ * Load multiple plugins with certain options.
+ *
+ * @param {Array<string>} plugins
+ * @param {Object<string, PluginLoader.PluginOption>} opts
+ */
+ loadPlugins(plugins = [], opts = {}) {
+ this._pluginListLoaded = true;
+
+ plugins.forEach(path => {
+ const url = this._urlFor(path);
+ // Skip if preloaded, for bundling.
+ if (this.isPluginPreloaded(url)) return;
+
+ const pluginKey = this._getPluginKeyFromUrl(url);
+ // Skip if already installed.
+ if (this._plugins.has(pluginKey)) return;
+ this._plugins.set(pluginKey, {
+ name: pluginKey,
+ url,
+ state: PluginState.PENDING,
+ plugin: null,
+ });
+
+ if (this._isPathEndsWith(url, '.html')) {
+ this._importHtmlPlugin(url, opts && opts[path]);
+ } else if (this._isPathEndsWith(url, '.js')) {
+ this._loadJsPlugin(url);
+ } else {
+ this._failToLoad(`Unrecognized plugin url ${url}`, url);
+ }
+ });
+
+ this.awaitPluginsLoaded().then(() => {
+ console.info('Plugins loaded');
+ this._getReporting().pluginsLoaded(this._getAllInstalledPluginNames());
+ });
+ }
+
+ _isPathEndsWith(url, suffix) {
+ if (!(url instanceof URL)) {
+ try {
+ url = new URL(url);
+ } catch (e) {
+ console.warn(e);
+ return false;
+ }
+ }
+
+ return url.pathname && url.pathname.endsWith(suffix);
+ }
+
+ _getAllInstalledPluginNames() {
+ const installedPlugins = [];
+ for (const plugin of this._plugins.values()) {
+ if (plugin.state === PluginState.LOADED) {
+ installedPlugins.push(plugin.name);
+ }
+ }
+ return installedPlugins;
+ }
+
+ install(callback, opt_version, opt_src) {
+ // HTML import polyfill adds __importElement pointing to the import tag.
+ const script = document.currentScript &&
+ (document.currentScript.__importElement || document.currentScript);
+ let src = opt_src || (script && script.src);
+ if (!src || src.startsWith('data:')) {
+ src = script && script.baseURI;
+ }
+
+ if (opt_version && opt_version !== API_VERSION) {
+ this._failToLoad(`Plugin ${src} install error: only version ` +
+ API_VERSION + ' is supported in PolyGerrit. ' + opt_version +
+ ' was given.', src);
+ return;
+ }
+
+ const pluginObject = this.getPlugin(src);
+ let plugin = pluginObject && pluginObject.plugin;
+ if (!plugin) {
+ plugin = new Plugin(src);
+ }
+ try {
+ callback(plugin);
+ this._pluginInstalled(src, plugin);
+ } catch (e) {
+ this._failToLoad(`${e.name}: ${e.message}`, src);
+ }
+ }
+
+ get arePluginsLoaded() {
+ // As the size of plugins is relatively small,
+ // so the performance of this check should be reasonable
+ if (!this._pluginListLoaded) return false;
+ for (const plugin of this._plugins.values()) {
+ if (plugin.state === PluginState.PENDING) return false;
+ }
+ return true;
+ }
+
+ _checkIfCompleted() {
+ if (this.arePluginsLoaded && this._loadingResolver) {
+ this._loadingResolver();
+ this._loadingResolver = null;
+ this._loadingPromise = null;
+ }
+ }
+
+ _timeout() {
+ const pendingPlugins = [];
+ for (const plugin of this._plugins.values()) {
+ if (plugin.state === PluginState.PENDING) {
+ this._updatePluginState(plugin.url, PluginState.LOAD_FAILED);
+ this._checkIfCompleted();
+ pendingPlugins.push(plugin.url);
+ }
+ }
+ return `Timeout when loading plugins: ${pendingPlugins.join(',')}`;
+ }
+
+ _failToLoad(message, pluginUrl) {
+ // Show an alert with the error
+ document.dispatchEvent(new CustomEvent('show-alert', {
+ detail: {
+ message: `Plugin install error: ${message} from ${pluginUrl}`,
+ },
+ }));
+ this._updatePluginState(pluginUrl, PluginState.LOAD_FAILED);
+ this._checkIfCompleted();
+ }
+
+ _updatePluginState(pluginUrl, state) {
+ const key = this._getPluginKeyFromUrl(pluginUrl);
+ if (this._plugins.has(key)) {
+ this._plugins.get(key).state = state;
+ } else {
+ // Plugin is not recorded for some reason.
+ console.warn(`Plugin loaded separately: ${pluginUrl}`);
+ this._plugins.set(key, {
+ name: key,
+ url: pluginUrl,
+ state,
+ plugin: null,
+ });
+ }
+ return this._plugins.get(key);
+ }
+
+ _pluginInstalled(url, plugin) {
+ const pluginObj = this._updatePluginState(url, PluginState.LOADED);
+ pluginObj.plugin = plugin;
+ this._getReporting().pluginLoaded(plugin.getPluginName() || url);
+ console.log(`Plugin ${plugin.getPluginName() || url} installed.`);
+ this._checkIfCompleted();
+ }
+
+ installPreloadedPlugins() {
+ if (!window.Gerrit || !window.Gerrit._preloadedPlugins) { return; }
+ const Gerrit = window.Gerrit;
+ for (const name in Gerrit._preloadedPlugins) {
+ if (!Gerrit._preloadedPlugins.hasOwnProperty(name)) { continue; }
+ const callback = Gerrit._preloadedPlugins[name];
+ this.install(callback, API_VERSION, PRELOADED_PROTOCOL + name);
+ }
+ }
+
+ isPluginPreloaded(pathOrUrl) {
+ const url = this._urlFor(pathOrUrl);
+ const name = getPluginNameFromUrl(url);
+ if (name && window.Gerrit._preloadedPlugins) {
+ return window.Gerrit._preloadedPlugins.hasOwnProperty(name);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Checks if given plugin path/url is enabled or not.
+ * @param {string} pathOrUrl
+ */
+ isPluginEnabled(pathOrUrl) {
+ const url = this._urlFor(pathOrUrl);
+ if (this.isPluginPreloaded(url)) return true;
+ const key = this._getPluginKeyFromUrl(url);
+ return this._plugins.has(key);
+ }
+
+ /**
+ * Returns the plugin object with a given url.
+ * @param {string} pathOrUrl
+ */
+ getPlugin(pathOrUrl) {
+ const key = this._getPluginKeyFromUrl(this._urlFor(pathOrUrl));
+ return this._plugins.get(key);
+ }
+
+ /**
+ * Checks if given plugin path/url is loaded or not.
+ * @param {string} pathOrUrl
+ */
+ isPluginLoaded(pathOrUrl) {
+ const url = this._urlFor(pathOrUrl);
+ const key = this._getPluginKeyFromUrl(url);
+ return this._plugins.has(key) ?
+ this._plugins.get(key).state === PluginState.LOADED :
+ false;
+ }
+
+ _importHtmlPlugin(pluginUrl, opts = {}) {
+ // onload (second param) needs to be a function. When null or undefined
+ // were passed, plugins were not loaded correctly.
+ (Polymer.importHref || Polymer.Base.importHref)(
+ this._urlFor(pluginUrl), () => {},
+ () => this._failToLoad(`${pluginUrl} import error`, pluginUrl),
+ !opts.sync);
+ }
+
+ _loadJsPlugin(pluginUrl) {
+ this._createScriptTag(this._urlFor(pluginUrl));
+ }
+
+ _createScriptTag(url) {
+ const el = document.createElement('script');
+ el.defer = true;
+ el.setAttribute('src', url);
+ el.onerror = () => this._failToLoad(`${url} load error`, url);
+ return document.body.appendChild(el);
+ }
+
+ _urlFor(pathOrUrl) {
+ if (!pathOrUrl) {
+ return pathOrUrl;
+ }
+ if (pathOrUrl.startsWith(PRELOADED_PROTOCOL) ||
+ pathOrUrl.startsWith('http')) {
+ // Plugins are loaded from another domain or preloaded.
+ return pathOrUrl;
+ }
+ if (!pathOrUrl.startsWith('/')) {
+ pathOrUrl = '/' + pathOrUrl;
+ }
+ return window.location.origin + getBaseUrl() + pathOrUrl;
+ }
+
+ awaitPluginsLoaded() {
+ // Resolve if completed.
+ this._checkIfCompleted();
+
+ if (this.arePluginsLoaded) {
+ return Promise.resolve();
+ }
+ if (!this._loadingPromise) {
+ let timerId;
+ this._loadingPromise =
+ Promise.race([
+ new Promise(resolve => this._loadingResolver = resolve),
+ new Promise((_, reject) => timerId = setTimeout(
+ () => {
+ reject(this._timeout());
+ }, PLUGIN_LOADING_TIMEOUT_MS)),
+ ]).then(() => {
+ if (timerId) clearTimeout(timerId);
+ });
+ }
+ return this._loadingPromise;
+ }
+ }
+
+ /**
+ * @typedef {{
+ * name:string,
+ * url:string,
+ * state:PluginState,
+ * plugin:Object
+ * }}
+ */
+ PluginLoader.PluginObject;
+
+ /**
+ * @typedef {{
+ * sync:boolean,
+ * }}
+ */
+ PluginLoader.PluginOption;
+
+ window.PluginLoader = PluginLoader;
+})(window);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
new file mode 100644
index 0000000..ee54319
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
@@ -0,0 +1,502 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-plugin-host</title>
+<script src="/test/common-test-setup.js"></script>
+<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+
+<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-js-api-interface.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <gr-js-api-interface></gr-js-api-interface>
+ </template>
+</test-fixture>
+
+<script>
+ const {PRELOADED_PROTOCOL, PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
+ suite('gr-plugin-loader tests', () => {
+ let plugin;
+ let sandbox;
+ let url;
+ let sendStub;
+
+ setup(() => {
+ this.clock = sinon.useFakeTimers();
+ sandbox = sinon.sandbox.create();
+ sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
+ stub('gr-rest-api-interface', {
+ getAccount() {
+ return Promise.resolve({name: 'Judy Hopps'});
+ },
+ send(...args) {
+ return sendStub(...args);
+ },
+ });
+ sandbox.stub(document.body, 'appendChild');
+ fixture('basic');
+ url = window.location.origin;
+ });
+
+ teardown(() => {
+ sandbox.restore();
+ this.clock.restore();
+ Gerrit._testOnly_resetPlugins();
+ });
+
+ test('reuse plugin for install calls', () => {
+ Gerrit.install(p => { plugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+
+ let otherPlugin;
+ Gerrit.install(p => { otherPlugin = p; }, '0.1',
+ 'http://test.com/plugins/testplugin/static/test.js');
+ assert.strictEqual(plugin, otherPlugin);
+ });
+
+ test('flushes preinstalls if provided', () => {
+ assert.doesNotThrow(() => {
+ Gerrit._testOnly_flushPreinstalls();
+ });
+ window.Gerrit.flushPreinstalls = sandbox.stub();
+ Gerrit._testOnly_flushPreinstalls();
+ assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce);
+ delete window.Gerrit.flushPreinstalls;
+ });
+
+ test('versioning', () => {
+ const callback = sandbox.spy();
+ Gerrit.install(callback, '0.0pre-alpha');
+ assert(callback.notCalled);
+ });
+
+ test('report pluginsLoaded', done => {
+ stub('gr-reporting', {
+ pluginsLoaded() {
+ done();
+ },
+ });
+ Gerrit._loadPlugins([]);
+ });
+
+ test('arePluginsLoaded', done => {
+ assert.isFalse(Gerrit._arePluginsLoaded());
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ Gerrit._loadPlugins(plugins);
+ assert.isFalse(Gerrit._arePluginsLoaded());
+ // Timeout on loading plugins
+ this.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
+
+ flush(() => {
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ done();
+ });
+ });
+
+ test('plugins installed successfully', done => {
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => void 0, undefined, url);
+ });
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ done();
+ });
+ });
+
+ test('isPluginEnabled and isPluginLoaded', done => {
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => void 0, undefined, url);
+ });
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ 'bar/static/test.js',
+ ];
+ Gerrit._loadPlugins(plugins);
+ assert.isTrue(
+ plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
+ );
+
+ flush(() => {
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(
+ plugins.every(plugin => Gerrit._pluginLoader.isPluginLoaded(plugin))
+ );
+
+ done();
+ });
+ });
+
+ test('plugins installed mixed result, 1 fail 1 succeed', done => {
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ const alertStub = sandbox.stub();
+ document.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => {
+ if (url === plugins[0]) {
+ throw new Error('failed');
+ }
+ }, undefined, url);
+ });
+
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(alertStub.calledOnce);
+ done();
+ });
+ });
+
+ test('isPluginEnabled and isPluginLoaded for mixed results', done => {
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ const alertStub = sandbox.stub();
+ document.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => {
+ if (url === plugins[0]) {
+ throw new Error('failed');
+ }
+ }, undefined, url);
+ });
+
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ Gerrit._loadPlugins(plugins);
+ assert.isTrue(
+ plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
+ );
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(alertStub.calledOnce);
+ assert.isTrue(Gerrit._pluginLoader.isPluginLoaded(plugins[1]));
+ assert.isFalse(Gerrit._pluginLoader.isPluginLoaded(plugins[0]));
+ done();
+ });
+ });
+
+ test('plugins installed all failed', done => {
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ const alertStub = sandbox.stub();
+ document.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => {
+ throw new Error('failed');
+ }, undefined, url);
+ });
+
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly([]));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(alertStub.calledTwice);
+ done();
+ });
+ });
+
+ test('plugins installed failed becasue of wrong version', done => {
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+
+ const alertStub = sandbox.stub();
+ document.addEventListener('show-alert', alertStub);
+
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => {
+ }, url === plugins[0] ? '' : 'alpha', url);
+ });
+
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ assert.isTrue(alertStub.calledOnce);
+ done();
+ });
+ });
+
+ test('multiple assets for same plugin installed successfully', done => {
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => void 0, undefined, url);
+ });
+ const pluginsLoadedStub = sandbox.stub();
+ stub('gr-reporting', {
+ pluginsLoaded: (...args) => pluginsLoadedStub(...args),
+ });
+
+ const plugins = [
+ 'http://test.com/plugins/foo/static/test.js',
+ 'http://test.com/plugins/foo/static/test2.js',
+ 'http://test.com/plugins/bar/static/test.js',
+ ];
+ Gerrit._loadPlugins(plugins);
+
+ flush(() => {
+ assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
+ assert.isTrue(Gerrit._arePluginsLoaded());
+ done();
+ });
+ });
+
+ suite('plugin path and url', () => {
+ let importHtmlPluginStub;
+ let loadJsPluginStub;
+ setup(() => {
+ importHtmlPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => {
+ importHtmlPluginStub(url);
+ });
+ loadJsPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ loadJsPluginStub(url);
+ });
+ });
+
+ test('invalid plugin path', () => {
+ const failToLoadStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_failToLoad', (...args) => {
+ failToLoadStub(...args);
+ });
+
+ Gerrit._loadPlugins([
+ 'foo/bar',
+ ]);
+
+ assert.isTrue(failToLoadStub.calledOnce);
+ assert.isTrue(failToLoadStub.calledWithExactly(
+ `Unrecognized plugin url ${url}/foo/bar`,
+ `${url}/foo/bar`
+ ));
+ });
+
+ test('relative path for plugins', () => {
+ Gerrit._loadPlugins([
+ 'foo/bar.js',
+ 'foo/bar.html',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`${url}/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`${url}/foo/bar.js`)
+ );
+ });
+
+
+ test('relative path should honor getBaseUrl', () => {
+ const testUrl = '/test';
+ sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl', () => {
+ return testUrl;
+ });
+
+ Gerrit._loadPlugins([
+ 'foo/bar.js',
+ 'foo/bar.html',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(
+ `${url}${testUrl}/foo/bar.html`
+ )
+ );
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`${url}${testUrl}/foo/bar.js`)
+ );
+ });
+
+ test('absolute path for plugins', () => {
+ Gerrit._loadPlugins([
+ 'http://e.com/foo/bar.js',
+ 'http://e.com/foo/bar.html',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.calledOnce);
+ assert.isTrue(
+ importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
+ );
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ assert.isTrue(
+ loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`)
+ );
+ });
+ });
+
+ test('adds js plugins will call the body', () => {
+ Gerrit._loadPlugins([
+ 'http://e.com/foo/bar.js',
+ 'http://e.com/bar/foo.js',
+ ]);
+ assert.isTrue(document.body.appendChild.calledTwice);
+ });
+
+ test('can call awaitPluginsLoaded multiple times', done => {
+ const plugins = [
+ 'http://e.com/foo/bar.js',
+ 'http://e.com/bar/foo.js',
+ ];
+
+ let installed = false;
+ function pluginCallback(url) {
+ if (url === plugins[1]) {
+ installed = true;
+ }
+ }
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ Gerrit.install(() => pluginCallback(url), undefined, url);
+ });
+
+ Gerrit._loadPlugins(plugins);
+
+ Gerrit.awaitPluginsLoaded().then(() => {
+ assert.isTrue(installed);
+
+ Gerrit.awaitPluginsLoaded().then(() => {
+ done();
+ });
+ });
+ });
+
+ suite('preloaded plugins', () => {
+ test('skips preloaded plugins when load plugins', () => {
+ const importHtmlPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => {
+ importHtmlPluginStub(url);
+ });
+ const loadJsPluginStub = sandbox.stub();
+ sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
+ loadJsPluginStub(url);
+ });
+
+ Gerrit._preloadedPlugins = {
+ foo: () => void 0,
+ bar: () => void 0,
+ };
+
+ Gerrit._loadPlugins([
+ 'http://e.com/plugins/foo.js',
+ 'plugins/bar.html',
+ 'http://e.com/plugins/test/foo.js',
+ ]);
+
+ assert.isTrue(importHtmlPluginStub.notCalled);
+ assert.isTrue(loadJsPluginStub.calledOnce);
+ });
+
+ test('isPluginPreloaded', () => {
+ Gerrit._preloadedPlugins = {baz: ()=>{}};
+ assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('plugins/foo/bar'));
+ assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('http://a.com/42'));
+ assert.isTrue(
+ Gerrit._pluginLoader.isPluginPreloaded(PRELOADED_PROTOCOL + 'baz')
+ );
+ Gerrit._preloadedPlugins = null;
+ });
+
+ test('preloaded plugins are installed', () => {
+ const installStub = sandbox.stub();
+ Gerrit._preloadedPlugins = {foo: installStub};
+ Gerrit._pluginLoader.installPreloadedPlugins();
+ assert.isTrue(installStub.called);
+ const pluginApi = installStub.lastCall.args[0];
+ assert.strictEqual(pluginApi.getPluginName(), 'foo');
+ });
+
+ test('installing preloaded plugin', () => {
+ let plugin;
+ window.ASSETS_PATH = 'http://blips.com/chitz';
+ Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
+ assert.strictEqual(plugin.getPluginName(), 'foo');
+ assert.strictEqual(plugin.url('/some/thing.html'),
+ 'http://blips.com/chitz/plugins/foo/some/thing.html');
+ delete window.ASSETS_PATH;
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
index 05f84c0..bcbd961 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
@@ -50,7 +50,6 @@
a[k] = (...args) => restApiStub[k](...args);
return a;
}, {}));
- Gerrit._setPluginsCount(1);
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
instance = new GrPluginRestApi();
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index d44acea..d76c983 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -267,10 +267,14 @@
return;
}
return this.registerCustomComponent(
- Gerrit._getPluginScreenName(this.getPluginName(), screenName),
+ this._getScreenName(screenName),
opt_moduleName);
};
+ Plugin.prototype._getScreenName = function(screenName) {
+ return `${this.getPluginName()}-screen-${screenName}`;
+ };
+
const deprecatedAPI = {
_loadedGwt: ()=> {},
@@ -321,7 +325,7 @@
'Please use strings for patterns.');
return;
}
- this.hook(Gerrit._getPluginScreenName(this.getPluginName(), pattern))
+ this.hook(this._getScreenName(pattern))
.onAttached(el => {
el.style.display = 'none';
callback({
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
index fa54dba..ad1a039 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
@@ -24,6 +24,7 @@
<link rel="import" href="/bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
<dom-module id="gr-textarea">
<template>
@@ -96,6 +97,7 @@
max-rows="[[maxRows]]"
value="{{text}}"
on-bind-value-changed="_onValueChanged"></iron-autogrow-textarea>
+ <gr-reporting id="reporting"></gr-reporting>
</template>
<script src="gr-textarea.js"></script>
</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index c4d31ac..f751a0d 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -82,7 +82,6 @@
_colonIndex: Number,
_currentSearchString: {
type: String,
- value: '',
observer: '_determineSuggestions',
},
_hideAutocomplete: {
@@ -113,7 +112,6 @@
},
ready() {
- this._resetEmojiDropdown();
if (this.monospace) {
this.classList.add('monospace');
}
@@ -153,6 +151,7 @@
e.stopPropagation();
this.$.emojiSuggestions.cursorUp();
this.$.textarea.textarea.focus();
+ this.disableEnterKeyForSelectingEmoji = false;
},
_handleDownKey(e) {
@@ -161,24 +160,34 @@
e.stopPropagation();
this.$.emojiSuggestions.cursorDown();
this.$.textarea.textarea.focus();
+ this.disableEnterKeyForSelectingEmoji = false;
},
_handleEnterByKey(e) {
- if (this._hideAutocomplete) { return; }
+ if (this._hideAutocomplete || this.disableEnterKeyForSelectingEmoji) {
+ return;
+ }
e.preventDefault();
e.stopPropagation();
- this.text = this._getText(this.$.emojiSuggestions.getCurrentText());
- this._resetEmojiDropdown();
+ this._setEmoji(this.$.emojiSuggestions.getCurrentText());
},
_handleEmojiSelect(e) {
- this.text = this._getText(e.detail.selected.dataset.value);
+ this._setEmoji(e.detail.selected.dataset.value);
+ },
+
+ _setEmoji(text) {
+ const colonIndex = this._colonIndex;
+ this.text = this._getText(text);
+ this.$.textarea.selectionStart = colonIndex + 1;
+ this.$.textarea.selectionEnd = colonIndex + 1;
+ this.$.reporting.reportInteraction('select-emoji');
this._resetEmojiDropdown();
},
_getText(value) {
return this.text.substr(0, this._colonIndex || 0) +
- value + this.text.substr(this.$.textarea.selectionStart) + ' ';
+ value + this.text.substr(this.$.textarea.selectionStart);
},
/**
* Uses a hidden element with the same width and styling of the textarea and
@@ -218,41 +227,41 @@
// If cursor is not in textarea (just opened with colon as last char),
// Don't do anything.
if (!e.currentTarget.focused) { return; }
- const newChar = e.detail.value[this.$.textarea.selectionStart - 1];
- // When a colon is detected, set a colon index, but don't do anything else
- // yet.
- if (newChar === ':') {
+ const charAtCursor = e.detail && e.detail.value ?
+ e.detail.value[this.$.textarea.selectionStart - 1] : '';
+ if (charAtCursor !== ':' && this._colonIndex == null) { return; }
+
+ // When a colon is detected, set a colon index.
+ if (charAtCursor === ':') {
this._colonIndex = this.$.textarea.selectionStart - 1;
- // If the colon index exists, continue to determine what needs to be done
- // with the dropdown. It may be open or closed at this point.
- } else if (this._colonIndex !== null) {
- // The search string is a substring of the textarea's value from (1
- // position after) the colon index to the cursor position.
- this._currentSearchString = e.detail.value.substr(this._colonIndex + 1,
- this.$.textarea.selectionStart);
- // Under the following conditions, close and reset the dropdown:
- // - The cursor is no longer at the end of the current search string
- // - The search string is an space or new line
- // - The colon has been removed
- // - There are no suggestions that match the search string
- if (this.$.textarea.selectionStart !==
- this._currentSearchString.length + this._colonIndex + 1 ||
- this._currentSearchString === ' ' ||
- this._currentSearchString === '\n' ||
- !(e.detail.value[this._colonIndex] === ':') ||
- !this._suggestions.length) {
- this._resetEmojiDropdown();
- // Otherwise open the dropdown and set the position to be just below the
- // cursor.
- } else if (this.$.emojiSuggestions.isHidden) {
- this._updateCaratPosition();
- }
- this.$.textarea.textarea.focus();
}
+
+ this._currentSearchString = e.detail.value.substr(this._colonIndex + 1,
+ this.$.textarea.selectionStart - this._colonIndex - 1);
+ // Under the following conditions, close and reset the dropdown:
+ // - The cursor is no longer at the end of the current search string
+ // - The search string is an space or new line
+ // - The colon has been removed
+ // - There are no suggestions that match the search string
+ if (this.$.textarea.selectionStart !==
+ this._currentSearchString.length + this._colonIndex + 1 ||
+ this._currentSearchString === ' ' ||
+ this._currentSearchString === '\n' ||
+ !(e.detail.value[this._colonIndex] === ':') ||
+ !this._suggestions.length) {
+ this._resetEmojiDropdown();
+ // Otherwise open the dropdown and set the position to be just below the
+ // cursor.
+ } else if (this.$.emojiSuggestions.isHidden) {
+ this._updateCaratPosition();
+ }
+ this.$.textarea.textarea.focus();
},
+
_openEmojiDropdown() {
this.$.emojiSuggestions.open();
+ this.$.reporting.reportInteraction('open-emoji-dropdown');
},
_formatSuggestions(matchedSuggestions) {
@@ -268,11 +277,14 @@
_determineSuggestions(emojiText) {
if (!emojiText.length) {
this._formatSuggestions(ALL_SUGGESTIONS);
+ this.disableEnterKeyForSelectingEmoji = true;
+ } else {
+ const matches = ALL_SUGGESTIONS.filter(suggestion => {
+ return suggestion.match.includes(emojiText);
+ }).slice(0, MAX_ITEMS_DROPDOWN);
+ this._formatSuggestions(matches);
+ this.disableEnterKeyForSelectingEmoji = false;
}
- const matches = ALL_SUGGESTIONS.filter(suggestion => {
- return suggestion.match.includes(emojiText);
- }).splice(0, MAX_ITEMS_DROPDOWN);
- this._formatSuggestions(matches);
},
_resetEmojiDropdown() {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index 077f4b7..854dda9 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -53,6 +53,7 @@
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
+ sandbox.stub(element.$.reporting, 'reportInteraction');
});
teardown(() => {
@@ -90,6 +91,21 @@
element.$.textarea.selectionStart = 1;
element.$.textarea.selectionEnd = 1;
element.text = ':';
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.emojiSuggestions.isHidden);
+ assert.equal(element._colonIndex, 0);
+ assert.isFalse(element._hideAutocomplete);
+ assert.equal(element._currentSearchString, '');
+ });
+
+ test('emoji selector opens when a colon is typed and some substring',
+ () => {
+ MockInteractions.focus(element.$.textarea);
+ // Needed for Safari tests. selectionStart is not updated when text is
+ // updated.
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ element.text = ':';
element.$.textarea.selectionStart = 2;
element.$.textarea.selectionEnd = 2;
element.text = ':t';
@@ -100,6 +116,30 @@
assert.equal(element._currentSearchString, 't');
});
+ test('emoji selector opens when a colon is typed in middle of text',
+ () => {
+ MockInteractions.focus(element.$.textarea);
+ // Needed for Safari tests. selectionStart is not updated when text is
+ // updated.
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ // Since selectionStart is on Chrome set always on end of text, we
+ // stub it to 1
+ const text = ': hello';
+ sandbox.stub(element.$, 'textarea', {
+ selectionStart: 1,
+ value: text,
+ textarea: {
+ focus: () => {},
+ },
+ });
+ element.text = text;
+ flushAsynchronousOperations();
+ assert.isFalse(element.$.emojiSuggestions.isHidden);
+ assert.equal(element._colonIndex, 0);
+ assert.isFalse(element._hideAutocomplete);
+ assert.equal(element._currentSearchString, '');
+ });
test('emoji selector closes when text changes before the colon', () => {
const resetStub = sandbox.stub(element, '_resetEmojiDropdown');
MockInteractions.focus(element.$.textarea);
@@ -161,7 +201,7 @@
const selectedItem = {dataset: {value: '😂'}};
const event = {detail: {selected: selectedItem}};
element._handleEmojiSelect(event);
- assert.equal(element.text, 'test test 😂 ');
+ assert.equal(element.text, 'test test 😂');
});
test('_updateCaratPosition', () => {
@@ -236,8 +276,21 @@
MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
assert.isTrue(enterSpy.called);
flushAsynchronousOperations();
- // A space is automatically added at the end.
- assert.equal(element.text, '💯 ');
+ assert.equal(element.text, '💯');
+ });
+
+ test('enter key - ignored on just colon without more information', () => {
+ const enterSpy = sandbox.spy(element.$.emojiSuggestions,
+ 'getCursorTarget');
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ assert.isFalse(enterSpy.called);
+ MockInteractions.focus(element.$.textarea);
+ element.$.textarea.selectionStart = 1;
+ element.$.textarea.selectionEnd = 1;
+ element.text = ':';
+ flushAsynchronousOperations();
+ MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13);
+ assert.isFalse(enterSpy.called);
});
});
});
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index a1c3ae6..cb1fcef 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -188,6 +188,7 @@
'shared/gr-js-api-interface/gr-js-api-interface_test.html',
'shared/gr-js-api-interface/gr-gerrit_test.html',
'shared/gr-js-api-interface/gr-plugin-action-context_test.html',
+ 'shared/gr-js-api-interface/gr-plugin-loader_test.html',
'shared/gr-js-api-interface/gr-plugin-endpoints_test.html',
'shared/gr-js-api-interface/gr-plugin-rest-api_test.html',
'shared/gr-fixed-panel/gr-fixed-panel_test.html',
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go
index 1a2d299..35bdefd 100644
--- a/polygerrit-ui/server.go
+++ b/polygerrit-ui/server.go
@@ -43,6 +43,8 @@
host = flag.String("host", "gerrit-review.googlesource.com", "Host to proxy requests to")
scheme = flag.String("scheme", "https", "URL scheme")
cdnPattern = regexp.MustCompile("https://cdn.googlesource.com/polygerrit_ui/[0-9.]*")
+ webComponentPattern = regexp.MustCompile("webcomponentsjs-p2")
+ grAppPattern = regexp.MustCompile("gr-app-p2")
bundledPluginsPattern = regexp.MustCompile("https://cdn.googlesource.com/polygerrit_assets/[0-9.]*")
)
@@ -184,9 +186,13 @@
buf.ReadFrom(reader)
original := buf.String()
+ // Replace the webcomponentsjs-p2 with webcomponentsjs
+ replaced := webComponentPattern.ReplaceAllString(original, "webcomponentsjs")
+ replaced = grAppPattern.ReplaceAllString(replaced, "gr-app")
+
// Simply remove all CDN references, so files are loaded from the local file system or the proxy
// server instead.
- replaced := cdnPattern.ReplaceAllString(original, "")
+ replaced = cdnPattern.ReplaceAllString(replaced, "")
// Modify window.INITIAL_DATA so that it has the same effect as injectLocalPlugins. To achieve
// this let's add JavaScript lines at the end of the <script>...</script> snippet that also
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 8d1e543..4240a9b 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-acceptance-framework</artifactId>
- <version>3.1.0-rc1</version>
+ <version>3.2.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Acceptance Test Framework</name>
<description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index 2787353..cf2b080 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-extension-api</artifactId>
- <version>3.1.0-rc1</version>
+ <version>3.2.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Extension API</name>
<description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index 695643c..7d3c4f0 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-api</artifactId>
- <version>3.1.0-rc1</version>
+ <version>3.2.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Plugin API</name>
<description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index b3e8ca5..9478283 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-war</artifactId>
- <version>3.1.0-rc1</version>
+ <version>3.2.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>Gerrit Code Review - WAR</name>
<description>Gerrit WAR</description>
diff --git a/version.bzl b/version.bzl
index 326e26e..fb1e5ca 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
# Used by :api_install and :api_deploy targets
# when talking to the destination repository.
#
-GERRIT_VERSION = "3.1.0-rc1"
+GERRIT_VERSION = "3.2.0-SNAPSHOT"