Merge branch 'stable-3.11'
* stable-3.11:
Point to correct JGit API docs version
Audit: fetch current user and sessionId at the beginning of the REST API
Release-Notes: skip
Change-Id: I6d70a0f3bb837330b944dd6143ecbed0e1041ff9
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 52b2b18..5b78353 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -428,6 +428,10 @@
Further documentation on how to push can be found on the
link:user-upload.html#push_create[Upload changes] page.
+**NOTE**: All change related permissions should normally set using
+"refs/heads/<ref>" branch and not using "refs/for/<ref>". Unless specifically
+stated otherwise.
+
[[access_categories]]
== Access Categories
@@ -1566,11 +1570,6 @@
using the link:rest-api-projects.html#check-access[check.access]
endpoint.
-In addition, when a request fails due to permission errors and the caller has
-this capability, ACL info is returned that contains information about the
-permissions rules that have been checked. This allows the user to understand
-which permissions rule caused request to be rejected.
-
[[capability_viewAllAccounts]]
=== View All Accounts
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 2ffc7bc..28aba16 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -104,6 +104,19 @@
[[accounts]]
=== Section accounts
+[[accounts.caseInsensitiveLocalPart]]accounts.caseInsensitiveLocalPart::
++
+When querying by email, the case sensitivity of the email local part depends on the domains
+specified in the this list.
++
+Users can register emails with mixed case, and if the email’s domain matches one in the configured
+list, the local part is treated as case-insensitive.
+This ensures that emails with different cases, such as User@mail.com and user@mail.com, are
+associated with the same user.
+For domains not listed in the configuration, email matching remains case-sensitive.
++
+Default is unset.
+
[[accounts.visibility]]accounts.visibility::
+
Controls visibility of other users' dashboard pages and
@@ -2205,6 +2218,23 @@
+
Default is `true`.
+[[core.useGitattributesForMerge]]core.useGitattributesForMerge::
++
+Use JGit's support for reading gitattributes files to control behavior during
+link:https://git-scm.com/docs/gitattributes.html#_performing_a_three_way_merge[
+three-way content merges,role=external,window=_blank]. This only affects
+projects that allow content merges.
++
+Enabling this support does add overhead to all content merge operations since
+the presence and content of in-tree `.gitattributes` files is always considered
+(even when the trees have no `.gitattributes` files or those files have no
+merge driver configuration). See the
+link:https://git-scm.com/docs/gitattributes.html[gitattributes man page] for
+more information on configuration and behavior. Also note that JGit only
+implements a subset of the documented configuration.
++
+Default is `false`.
+
[[core.repositoryCacheCleanupDelay]]core.repositoryCacheCleanupDelay::
+
Delay between each periodic cleanup of expired repositories.
@@ -2600,8 +2630,10 @@
[[gerrit.listProjectsFromIndex]]gerrit.listProjectsFromIndex::
+
-Enable rendering of project list from the secondary index instead
-of purely relying on the in-memory cache.
+Enable rendering of project list from the secondary index when the project
+filter is empty, instead of purely relying on the in-memory cache.
+When listing the projects with a filter, the list is always rendered
+from the project in-memory cache.
+
By default `false`.
+
@@ -2615,7 +2647,7 @@
link:access-control.html#capability_queryLimit[queryLimit]
which is defaulted to 500 entries.
-[[gerrit.projectStatePredicateEnabled]]
+[[gerrit.projectStatePredicateEnabled]]gerrit.projectStatePredicateEnabled::
+
Indicates whether the link:rest-api-projects.html[/projects/] REST API endpoint
supports filtering projects by state. The value is exposed in
@@ -2946,6 +2978,15 @@
+
Setting it to `true` may lead to some unexpected results in audit log and must be set carefully.
+[[groups.enableDeleteGroup]]groups.enableDeleteGroup::
++
+Controls whether to activate the delete groups functionality.
+If `true`, then users with permission can delete groups.
+This setting provides administrators the ability to activate or to deactivate delete groups functionality.
++
+By default, `false`.
++
+
[[groups.includeExternalUsersInRegisteredUsersGroup]]groups.includeExternalUsersInRegisteredUsersGroup::
+
Controls whether external users (these are users we have sufficient
@@ -3697,6 +3738,36 @@
+
Defaults to `false`.
+[[label]]
+=== Section label
+
+[[label.name.labelCopyEnforcement]]label.<name>.labelCopyEnforcement::
++
+The votes that satisfy this condition are copied regardless of label's
+link:config-labels.html#label_copyCondition[copyCondition].
++
+Uses the same syntax as label's
+link:config-labels.html#label_copyCondition[copyCondition]
++
+The final copyCondition is equivalent to
+----
+(<labelCondition> AND NOT <copyRestriction>) OR <copyEnforcement>
+----
+
+[[label.name.labelCopyRestriction]]label.<name>.labelCopyRestriction::
++
+The votes that satisfy this condition are not copied regardless of label's
+link:config-labels.html#label_copyCondition[copyCondition] unless they also
+satisfy "change.codeReviewLabelCopyEnforcement".
++
+Uses the same syntax as label's
+link:config-labels.html#label_copyCondition[copyCondition]
++
+The final copyCondition is equivalent to
+----
+(<labelCondition> AND NOT <copyRestriction>) OR <copyEnforcement>
+----
+
[[scheduledIndexer]]
=== Section scheduledIndexer
@@ -5745,9 +5816,8 @@
[[sshd.waitTimeout]]sshd.waitTimeout::
+
-Time in seconds after which the server automatically terminates
-connections waiting for a server operation to complete, like for instance
-cloning a very large repo with lots of refs.
+Maximum time the server will wait for available space in
+the output stream's buffer when writing data.
Values should use common unit suffixes to express their setting:
+
* s, sec, second, seconds
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
index b7493a3..ca80db92 100644
--- a/Documentation/config-labels.txt
+++ b/Documentation/config-labels.txt
@@ -438,9 +438,9 @@
`changekind:REWORK` is equivalent to setting `is:ANY`.
[[is_magic]]
-==== is:{MIN,MAX,ANY}
+==== is:{MIN,MAX,POSITIVE,NEGATIVE,ANY}
-Matches approvals that have a minimal, maximal or any score:
+Matches approvals that have a minimal, maximal, positive, negative or any score:
* [[is_min]]`MIN`:
+
@@ -452,6 +452,14 @@
Matches approvals that a maximal score, i.e. the highest possible
(positive) value for this label.
+* [[is_positive]]`POSITIVE`:
++
+Matches approvals that have score larger than 0.
+
+* [[is_negative]]`NEGATIVE`:
++
+Matches approvals that have score smaller than 0.
+
* [[is_any]]`ANY`:
+
Matches any approval when a new patch set is uploaded.
@@ -469,12 +477,20 @@
Matches votes granted by a user who is a member of
link:#group-id[\{group-id\}].
+Plugins can install custom operands for "uploaderin" that are checked before
+group membership is checked and have format of
+"uploaderin:<operand>_<pluginName>"
+
[[uploaderin]]
==== uploaderin:link:#group-id[\{group-id\}]
Matches all votes if the new patch set was uploaded by a member of
link:#group-id[\{group-id\}].
+Plugins can install custom operands for "uploaderin" that are checked before
+group membership is checked and have format of
+"uploaderin:<operand>_<pluginName>"
+
[[has_unchanged_files]]
==== has:unchanged-files
@@ -495,6 +511,13 @@
Note, "unchanged-files" is the only value that is supported for the
"has" operator.
+[[changeis]]
+==== changeis:{Change Query is: predicate}
+
+Any "is:{something}" predicate that is available as part of
+link:user-search.html#search-operators[Change Query] can be used in copy
+condition with "changeis:" prefix.
+
[[group-id]]
==== Group ID
diff --git a/Documentation/cross-repository-changes.txt b/Documentation/cross-repository-changes.txt
index 53fd5cd..16067d5 100644
--- a/Documentation/cross-repository-changes.txt
+++ b/Documentation/cross-repository-changes.txt
@@ -32,7 +32,8 @@
* A topic is a string that can be associated with a change.
* Multiple changes can use that topic to be submitted at the same time (assuming
approvals, etc.).
-* Submitting a change with a topic causes all of the changes in the topic *to be
+* When link:config-gerrit.html#change.submitWholeTopic[config.submitWholeTopic] is enabled,
+ submitting a change within a topic causes all of the changes in the topic *to be
submitted together*
** Topics that span only a single repository are guaranteed to be submitted
together
diff --git a/Documentation/dev-ci.txt b/Documentation/dev-ci.txt
new file mode 100644
index 0000000..c5a36a2
--- /dev/null
+++ b/Documentation/dev-ci.txt
@@ -0,0 +1,61 @@
+:linkattrs:
+= Gerrit Code Review - Continuous Integration
+
+[[summary]]
+== TL;DR
+
+All the Gerrit incoming changes and stable branches are built on the
+link:https://gerrit-ci.gerritforge.com[Gerrit CI].
+
+The link:https://gerrit.googlesource.com/gerrit-ci-scripts[gerrit-ci-scripts]
+project contains all the YAML files definitions associated with the
+link:https://docs.openstack.org/infra/jenkins-job-builder/attic/[Jenkins Job Builder]
+definition of the continuous integration Jobs.
+
+Gerrit maintainers are responsible for making sure that the CI jobs are
+up-to-date by triggering the
+link:https://gerrit-ci.gerritforge.com/job/gerrit-ci-scripts/[Gerrit-CI scripts job]
+upon new commits to the master branch of the gerrit-ci-scripts project.
+
+[[sign-up]]
+== Signing up as maintainer on Gerrit-CI
+
+The link:https://gerrit-ci.gerritforge.com/job/gerrit-ci-scripts/[Gerrit-CI]
+controller allows the Gerrit maintainers to sign-in using their GitHub
+accounts and have their username defined in the list of Users.
+
+*****
+NOTE: Because of recent link:https://docs.google.com/document/d/1vDjunjDrLYYpVoVON-B_c83f56Nhm-lMDMjXmYmFYk4[security issues]
+ found on Jenkins and future potential risks, only the Gerrit
+ maintainers and contributors are allowed to access the Jenkins UI and
+ sign-up for creating an account.
+*****
+
+Once the sign-up phase is complete, the maintainer needs to grant
+himself permissions on Jenkins by creating a change to add their names into
+the Jenkins
+link:https://gerrit.googlesource.com/gerrit-ci-scripts/+/refs/heads/master/jenkins-docker/server/config-external.xml#11[config.xml]
+in the permissions XML Section.
+
+== Applying changes to Jenkins on Gerrit-CI
+
+The Jenkins setup link:https://gerrit-ci.gerritforge.com[Gerrit-CI] adopts
+a link:https://www.ncsc.gov.uk/collection/zero-trust-architecture[Zero-Trust-Architecture]
+and therefore assumes that any access could be potentially malicious.
+
+- To limit the impact of future attacks or zero-days vulnerabilities the controller
+ must not have any meaningful secret or key which could be stolen.
+- It must not be possible for anyone to change anything on the Gerrit-CI
+ infrastructure without authenticating with their credentials.
+- No credentials should be stored anywhere on the Jenkins controller.
+- Everything should be coming from the link:https://gerrit.googlesource.com/gerrit-ci-scripts[gerrit-ci-scripts] project
+ and the infrastructure must be immutable and ephemeral.
+
+Gerrit maintainers can apply the latest changes on the Jenkins controller on Gerrit-CI by performing the following
+actions:
+
+- Generate a personal API account token by authenticating to
+ link:https://gerrit-ci.gerritforge.com/user/lucamilanesio/configure[Gerrit CI user's settings]
+ and generating a new API token.
+- Trigger the link:https://gerrit-ci.gerritforge.com/job/gerrit-ci-scripts/build?delay=0sec[gerrit-ci-scripts] job
+ entering their GitHub username and their API account token
diff --git a/Documentation/dev-community.txt b/Documentation/dev-community.txt
index 07e3a11..f26ca0d 100644
--- a/Documentation/dev-community.txt
+++ b/Documentation/dev-community.txt
@@ -58,6 +58,7 @@
[[maintainer]]
== Maintainer
+* link:dev-ci.html[Gerrit CI]
* link:dev-release.html[Making a Gerrit Release]
* link:dev-release-subproject.html[Making a Release of a Gerrit Subproject]
* link:https://www.gerritcodereview.com/publishing.html[Publish Gerrit Homepage]
diff --git a/Documentation/dev-crafting-changes.txt b/Documentation/dev-crafting-changes.txt
index 07aff36..9a97aad 100644
--- a/Documentation/dev-crafting-changes.txt
+++ b/Documentation/dev-crafting-changes.txt
@@ -158,7 +158,7 @@
and contentious discussions about trivial issues like whitespace.
You may download and run `google-java-format` on your own, or you may
-run `./tools/setup_gjf.sh` to download a local copy and set up a
+run `./tools/gjf.sh setup` to download a local copy and set up a
wrapper script. If you run your own copy, please use the same version,
as there may be slight differences between versions.
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 364dc9b..eef8b0d 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -1020,7 +1020,7 @@
@Override
protected void configure() {
bind(ChangeHasOperandFactory.class)
- .annotatedWith(Exports.named("sample")
+ .annotatedWith(Exports.named("sample"))
.to(SampleHasOperand.class);
}
}
@@ -1033,6 +1033,43 @@
}
----
+[[copy_condition_operands]]
+== Label Copy-Condition Operands
+
+Plugins can define operands to extend what votes are copied as part of label's
+link:config-labels.html#label_copyCondition[copyCondition].
+Plugin defines a Predicate<ApprovalContext> implementation and a factory that is
+bound to the `DynamicSet` from a module's `configure()` method in the plugin.
+
+Following operators supported:
+* "uploaderin:" and "approverin" by implementing UserInOperandFactory.
+
+The new operand, when used in a CopyCondition would appear as:
+ `:operandName_pluginName`
+
+A sample `UserInOperandFactory` class implementing, and registering, a
+new `uploaderin:sample_pluginName` operand is shown below:
+
+[source, java]
+----
+public class SampleUserInOperand implements UserInOperandFactory {
+ public static class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ bind(UserInOperandFactory.class)
+ .annotatedWith(Exports.named("sample"))
+ .to(SampleUserInOperand.class);
+ }
+ }
+
+ @Override
+ public Predicate<ApprovalContext> create()
+ throws QueryParseException {
+ return new UserInSamplePredicate();
+ }
+}
+----
+
[[command_options]]
== Command Options
@@ -3330,6 +3367,24 @@
`com.google.gerrit.server.RequestListener` is an extension point that is
invoked each time the server executes a request from a user.
+[[validation-options-listener]]
+== ValidationOptionsListener
+
+`com.google.gerrit.server.ValidationOptionsListener` is an extension point that
+is invoked when a patch set is created. The extension point gets the validation
+options that were specified by the user. For example, this extension point can
+be used to log validation options for auditing purposes.
+
+[[commit-validation-info-listener]]
+== CommitValidationInfoListener
+
+`com.google.gerrit.server.git.validators.ValidationOptionsListener` is an
+extension point that is invoked after a commit has passed the validations that
+are done by `CommitValidationListener`'s.
+
+If any `CommitValidationListener` rejects the commit (by throwing a
+`CommitValidationException`) this extension point is not invoked.
+
[[custom-keyed-values]]
== Custom Keyed values
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index a510e25..903c635 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -321,6 +321,17 @@
** `view`:
view implementation class
+=== SSH
+
+* `ssh/success_count`: Rate of successful SSH requests
+** `command_name`:
+ Name of the SSH command
+* `ssh/error_count`: Rate of SSH error responses
+** `command_name`:
+ Name of the SSH command
+** `exception`:
+ Name of the exception which has caused the request to fail.
+
=== Query
* `query/query_latency`: Successful query latency, accumulated over the life
diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt
index 7c93cc0..25eedf2 100644
--- a/Documentation/pg-plugin-dev.txt
+++ b/Documentation/pg-plugin-dev.txt
@@ -207,11 +207,6 @@
Supported events:
-* `history`: Invoked when the view is changed to a new screen within
- the Gerrit web application. The token after "#" is passed as the
- argument to the callback function, for example "/c/42/" while
- showing change 42.
-
* `showchange`: Invoked when a change is made visible. A
link:rest-api-changes.html#change-info[ChangeInfo] and
link:rest-api-changes.html#revision-info[RevisionInfo]
@@ -229,19 +224,6 @@
shown, and called again when the submit is confirmed to check whether
the actual submission action can proceed.
-* `comment`: Invoked when a DOM element that represents a comment is
- created. This DOM element is passed as argument. This DOM element
- contains nested elements that Gerrit uses to format the comment. The
- DOM structure may differ between comment types such as inline
- comments, file-level comments and summary comments, and it may change
- with new Gerrit versions.
-
-* `highlightjs-loaded`: Invoked when the highlight.js library has
- finished loading. The global `hljs` object (also now accessible via
- `window.hljs`) is passed as an argument to the callback function.
- This event can be used to register a new language highlighter with
- the highlight.js library before syntax highlighting begins.
-
[[high-level-api]]
== High-level API
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 58f2a5c..6b4ec94 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -19,6 +19,11 @@
provided by the `q` parameter. The `n` parameter can be used to limit
the returned results.
+[NOTE]
+Searching for accounts by email address can have different case sensitivity
+behavior depending on site configuration. See
+link:user-search-accounts.html#email[email operator] for details.
+
As result a list of link:#account-info[AccountInfo] entities is
returned.
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index c199d82..50055b9 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -5039,6 +5039,9 @@
can expand the ZIP to obtain the plain text patch, avoiding the
need for a base64 decoding step. This option implies `download`.
+Adding query parameter `raw` (for example `/changes/.../patch?raw`) will return
+the patch as a plain-text patch file.
+
Query parameter `download` (e.g. `/changes/.../patch?download`)
will suggest the browser save the patch as `commitsha1.diff.base64`,
for later processing by command line tools.
@@ -7803,6 +7806,57 @@
link:#notify-info[NotifyInfo] entity.
|=============================
+[[conflicts-info]]
+=== ConflictsInfo
+The `ConflictsInfo` entity contains information about conflicts in a revision.
+
+[options="header",cols="1,^1,5"]
+|==================================
+|Field Name ||Description
+|`ours` |optional|
+The SHA1 of the commit that was used as "ours" for the Git merge that created the revision. +
+- For merge commits that are created by the link:#create-change[Create Change] REST endpoint
+"ours" is the SHA1 of the change's target branch (the branch that is specified as `branch` in the
+link:#change-input[ChangeInput]). +
+- For merge commits that are created by the link:#create-merge-patch-set-for-change[Create Merge
+Patch Set For Change] REST endpoint "ours" is the SHA1 of the change's target branch (if in the
+link:#merge-patch-set-input[MergePatchSetInput] `inherit_parent` is `false` and `base_change` is
+not set), the current parent of the change (if in the
+link:#merge-patch-set-input[MergePatchSetInput] `inherit_parent` is `true`) or the current
+revision of the base change (if in the link:#merge-patch-set-input[MergePatchSetInput]
+`inherit_parent` is `false` and `base_change` is set). +
+- For the link:#cherry-pick[Cherry Pick Revision] and the
+link:rest-api-projects.html#cherry-pick-commit[Cherry Pick Commit] REST endpoints "ours" is the
+SHA1 of the commit onto which the revision/commit is being cherry-picked (e.g. the head of the
+target branch or the revision of the base change). +
+- For the link:#rebase-change[Rebase Change] and the link:#rebase-chain[Rebase Chain] REST
+endpoints "ours" is the SHA1 of the patch set that is being rebased. +
+Guaranteed to be set if `contains_conflicts` is `true`. If `contains_conflicts` is `false`, only
+set if the revision was created by Gerrit as a result of performing a Git merge.
+|`theirs` |optional|
+The SHA1 of the commit that was used as "theirs" for the Git merge that created the revision. +
+- For merge commits that are created by the the link:#create-change[Create Change] and the
+link:#create-merge-patch-set-for-change[Create Merge Patch Set For Change] REST endpoints "theirs"
+is the SHA1 of the source branch (the branch that is specified as `source` in the
+link:#merge-input[MergeInput]). +
+- For the link:#cherry-pick[Cherry Pick Revision] and the
+link:rest-api-projects.html#cherry-pick-commit[Cherry Pick Commit] REST endpoints "theirs" is the
+SHA1 of the revision/commit that is being cherry-picked. +
+- For the link:#rebase-change[Rebase Change] and the link:#rebase-chain[Rebase Chain] REST endpoints
+"theirs" is the SHA1 of the new base onto which the patch set is being rebased. +
+Guaranteed to be set if `contains_conflicts` is `true`. If `contains_conflicts` is `false`, only
+set if the revision was created by Gerrit as a result of performing a Git merge.
+|`contains_conflicts` ||
+Whether any of the files in the revision has a conflict due to merging "ours" and "theirs". +
+If "true" at least one of the files in the revision has a conflict and contains Git conflict
+markers. The conflicts occurred while performing a merge between "ours" and "theirs". +
+If "false", and "ours" and "theirs" are present, merging "ours" and "theirs" didn't have any
+conflict. In this case the files in the revision may only contain Git conflict markers if they
+were already present in "ours" or "theirs". +
+If "false", and "ours" and "theirs" are not present, the revision was not created as a result of
+performing a Git merge and hence doesn't contain conflicts.
+|=================================
+
[[delete-change-message-input]]
=== DeleteChangeMessageInput
The `DeleteChangeMessageInput` entity contains the options for deleting a change message.
@@ -9025,6 +9079,22 @@
|`description` |optional|
The description of this patchset, as displayed in the patchset
selector menu. May be null if no description is set.
+|`conflicts` |optional|
+Information about conflicts in this revision as a
+link:#conflicts-info[ConflictsInfo] entity. +
+Only set for revisions that were created by Gerrit as a result of
+performing a Git merge (merge commits that were created by the
+link:#create-change[Create Change] and the
+link:#create-merge-patch-set-for-change[Create Merge Patch Set For
+Change] REST endpoints and revisions created by the
+link:#cherry-pick[Cherry Pick Revision],
+link:rest-api-projects.html#cherry-pick-commit[Cherry Pick Commit],
+link:#rebase-change[Rebase Change] or link:#rebase-chain[Rebase Chain]
+REST endpoints). +
+Absence of this field, doesn't guarantee absence of conflicts. It can
+also be missing for revisions with conflicts that were created before
+the field was introduced or where the merge was performed locally (not
+by Gerrit operation).
|===========================
[[robot-comment-info]]
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index 52c505a..03ccd36 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -1479,6 +1479,47 @@
HTTP/1.1 204 No Content
----
+[[delete-group]]
+=== Delete Group
+--
+'DELETE /groups/link:#group-id[\{group-id\}]'
+--
+
+Delete group.
+The group to delete must be internal group.
+
+This endpoint is only allowed for Gerrit's internal groups; attempting to call on a
+non-internal group will return 405 Method Not Allowed.
+
+.Request
+----
+ DELETE /groups/MyProject-Committers HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
+[[delete-group]]
+=== Delete Group
+--
+'POST /groups/link:#group-id[\{group-id\}].delete'
+--
+
+Delete group.
+The deleted group must be internal group.
+
+.Request
+----
+ POST /groups/MyProject-Committers.delete HTTP/1.0
+----
+
+.Response
+----
+ HTTP/1.1 204 No Content
+----
+
[[ids]]
== IDs
diff --git a/Documentation/user-attention-set.txt b/Documentation/user-attention-set.txt
index 469fcdd..a618f1f 100644
--- a/Documentation/user-attention-set.txt
+++ b/Documentation/user-attention-set.txt
@@ -26,7 +26,7 @@
On the plus side you can strictly ignore everyone else's changes, if you are not
in the attention set. :-)
-=== Rules
+== Rules
To help with the back and forth, Gerrit applies some basic automated rules for
changing the attention set:
@@ -66,7 +66,7 @@
Note that just uploading a new patchset is not a relevant event for the
attention set to change.
-=== Interaction
+== Interaction
There are three ways to interact with the attention set: The attention icon,
the hovercard of owner and reviewer chips and the "Reply" dialog.
@@ -95,15 +95,15 @@
image::images/user-attention-set-reply-select.png["reply dialog section for selecting users", align="center"]
-=== Bots [[bots]]
+== Bots [[bots]]
The attention set is meant for human reviews only. Triggering bots and reacting
-to their results is a different workflow and not in scope of the attenion set.
-Thus members of the "Service Users" group will never be added to the
+to their results is a different workflow and not in scope of the attention set.
+Thus, members of the "Service Users" group will never be added to the
attention set. And replies by such users will only add the change owner (and
uploader) to the attention set, if it comes along with a negative vote.
-=== Dashboard
+== Dashboard
The default *dashboard* contains a new section at the top called "Your turn". It
lists all changes where the logged-in user is in the attention set. When you are
@@ -121,7 +121,14 @@
Note that you can also navigate to other users' dashboards to check their
"Your turn" section.
-=== Emails
+=== Bold Changes / Mark Reviewed
+
+Before the attention set feature, changes were bolded in the dashboard when
+*something* happened and you could explicitly "mark a change reviewed" on the
+change page. This former way of keeping track of what you should look at has
+been replaced by the attention set.
+
+== Emails
Every email begins with `Attention is currently required from: ...`, so you can
identify at a glance whether you are expected to act.
@@ -139,7 +146,7 @@
Gerrit-Attention: Marian Harbach <mharbach@google.com>
----
-=== Browser notifications
+== Browser notifications
You'll automatically get notifications when you are in the attention set. You
must enable desktop notifications on your browser to see them.
@@ -160,18 +167,11 @@
- Make sure browser notifications are turned on in your operating system
- Your host can have browser notifications disabled for some user groups
-=== Bold Changes / Mark Reviewed
-
-Before the attention set feature, changes were bolded in the dashboard when
-*something* happened and you could explicitly "mark a change reviewed" on the
-change page. This former way of keeping track of what you should look at has
-been replaced by the attention set.
-
-=== For Gerrit Admins
+== For Gerrit Admins
The Attention Set has been available since the 3.3 release (late 2020).
-=== Important note for all host owners, project owners, and bot owners
+== Important note for all host owners, project owners, and bot owners
If you are a host/project owner, please make sure all bots that run against your
host/project are part of the link:access-control.html#service_users[Service Users] group.
@@ -190,20 +190,13 @@
To add members or subgroups, use the Gerrit UI; BROWSE -> Groups ->
search for "Service Users" -> Members.
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
+=== Auto re-add owner [[auto-readd-owner]]
-SEARCHBOX
----------
+This job automatically re-adds the change owner to the attention-set for open non-WIP/private
+changes that have been inactive for a defined time. Gerrit administrators may
+link:config-gerrit.html#auto-readd[configure] this.
-=== Auto readd owner [[auto-readd-owner]]
-
-This job automatically readds the change owner to the attention-set for open non-WIP/private
-changes that have been inactive for a defined time. Gerrit administrators may configure
-link:config-gerrit.html#auto-readd[this]
-
-Readding the owner to the attention-set of an inactive change has the advantages:
+Re-adding the owner to the attention-set of an inactive change has the advantages:
* It signals the change owner that the review is not progressing and that the owner
may need to adjust the attention-set or indicate a need for a priority review.
@@ -211,3 +204,9 @@
* It makes people set changes in WIP or private for changes that should not
be actively reviewed.
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
+SEARCHBOX
+---------
diff --git a/Documentation/user-search-accounts.txt b/Documentation/user-search-accounts.txt
index d5318c9..07185c3 100644
--- a/Documentation/user-search-accounts.txt
+++ b/Documentation/user-search-accounts.txt
@@ -36,6 +36,13 @@
+
Matches accounts that have the email address 'EMAIL' or an email
address that starts with 'EMAIL'.
++
+If 'EMAIL' contains a domain and that domain matches one in the configured
+link:config-gerrit.html#accounts.caseInsensitiveLocalPart[accounts.caseInsensitiveLocalPart]
+list, the local part of the email will be treated as case-insensitive.
+For example, if `example.com` is configured as case-insensitive, then `User@example.com`
+and `user@example.com` will be treated as equivalent.
+For domains not listed, the matching will remain case-sensitive.
[[is]]
[[is-active]]
diff --git a/contrib/maintenance/.flake8 b/contrib/maintenance/.flake8
new file mode 100644
index 0000000..577b437
--- /dev/null
+++ b/contrib/maintenance/.flake8
@@ -0,0 +1,4 @@
+[flake8]
+max-line-length = 80
+extend-select = B950
+extend-ignore = E203,E402,E501,E701
diff --git a/contrib/maintenance/Pipfile b/contrib/maintenance/Pipfile
new file mode 100644
index 0000000..dd5279c
--- /dev/null
+++ b/contrib/maintenance/Pipfile
@@ -0,0 +1,16 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[dev-packages]
+black = "*"
+pytest = "*"
+flake8 = "*"
+flake8-bugbear = "*"
+
+[requires]
+python_version = "3.12"
+
+[pipenv]
+allow_prereleases = true
diff --git a/contrib/maintenance/Pipfile.lock b/contrib/maintenance/Pipfile.lock
new file mode 100644
index 0000000..3982b7d
--- /dev/null
+++ b/contrib/maintenance/Pipfile.lock
@@ -0,0 +1,165 @@
+{
+ "_meta": {
+ "hash": {
+ "sha256": "acb9add5d8f9c6fbe267f6ce332593ce25e13f68abd85d82095b17869127f80e"
+ },
+ "pipfile-spec": 6,
+ "requires": {
+ "python_version": "3.12"
+ },
+ "sources": [
+ {
+ "name": "pypi",
+ "url": "https://pypi.org/simple",
+ "verify_ssl": true
+ }
+ ]
+ },
+ "default": {},
+ "develop": {
+ "attrs": {
+ "hashes": [
+ "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346",
+ "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==24.2.0"
+ },
+ "black": {
+ "hashes": [
+ "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6",
+ "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e",
+ "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f",
+ "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018",
+ "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e",
+ "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd",
+ "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4",
+ "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed",
+ "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2",
+ "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42",
+ "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af",
+ "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb",
+ "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368",
+ "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb",
+ "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af",
+ "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed",
+ "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47",
+ "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2",
+ "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a",
+ "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c",
+ "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920",
+ "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==24.8.0"
+ },
+ "click": {
+ "hashes": [
+ "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
+ "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==8.1.7"
+ },
+ "flake8": {
+ "hashes": [
+ "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38",
+ "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.8.1'",
+ "version": "==7.1.1"
+ },
+ "flake8-bugbear": {
+ "hashes": [
+ "sha256:25bc3867f7338ee3b3e0916bf8b8a0b743f53a9a5175782ddc4325ed4f386b89",
+ "sha256:9b77627eceda28c51c27af94560a72b5b2c97c016651bdce45d8f56c180d2d32"
+ ],
+ "index": "pypi",
+ "markers": "python_full_version >= '3.8.1'",
+ "version": "==24.8.19"
+ },
+ "iniconfig": {
+ "hashes": [
+ "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
+ "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
+ ],
+ "markers": "python_version >= '3.7'",
+ "version": "==2.0.0"
+ },
+ "mccabe": {
+ "hashes": [
+ "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
+ "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
+ ],
+ "markers": "python_version >= '3.6'",
+ "version": "==0.7.0"
+ },
+ "mypy-extensions": {
+ "hashes": [
+ "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
+ "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
+ ],
+ "markers": "python_version >= '3.5'",
+ "version": "==1.0.0"
+ },
+ "packaging": {
+ "hashes": [
+ "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
+ "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==24.1"
+ },
+ "pathspec": {
+ "hashes": [
+ "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
+ "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==0.12.1"
+ },
+ "platformdirs": {
+ "hashes": [
+ "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907",
+ "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==4.3.6"
+ },
+ "pluggy": {
+ "hashes": [
+ "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1",
+ "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==1.5.0"
+ },
+ "pycodestyle": {
+ "hashes": [
+ "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3",
+ "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==2.12.1"
+ },
+ "pyflakes": {
+ "hashes": [
+ "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f",
+ "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"
+ ],
+ "markers": "python_version >= '3.8'",
+ "version": "==3.2.0"
+ },
+ "pytest": {
+ "hashes": [
+ "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181",
+ "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"
+ ],
+ "index": "pypi",
+ "markers": "python_version >= '3.8'",
+ "version": "==8.3.3"
+ }
+ }
+}
diff --git a/contrib/maintenance/README.md b/contrib/maintenance/README.md
new file mode 100644
index 0000000..e1cd5b3
--- /dev/null
+++ b/contrib/maintenance/README.md
@@ -0,0 +1,277 @@
+# Gerrit Maintenance
+
+This package provides a set of tools that can be used to maintain a Gerrit site.
+Some tools will also work with git repositories in general.
+
+The following tools are available:
+
+- [Extended Git GarbageCollection](#extended-git-garbagecollection)
+
+## Dependencies
+
+- Python > 3.12
+
+For development, some additional python libraries are required. These are managed
+with pipenv. To install them, run:
+
+```sh
+pipenv sync --dev
+```
+
+## Development
+
+### Code Style
+
+This package is formatted using `black`. To automatically format all python files,
+run:
+
+```sh
+pipenv run black .
+```
+
+`flake8` is being used to identify code style issues. To run it, use:
+
+```sh
+pipenv run flake8 .
+```
+
+### Tests
+
+To execute tests, run:
+
+```sh
+pipenv run pytest
+```
+
+## Usage
+
+The gerrit-maintenance CLI provides a toolbox to run scripts for performing
+maintenance tasks on a Gerrit site. The CLI uses a nested command structure. The
+available commands will be described in the following sections.
+
+To start the CLI, run:
+
+```sh
+pipenv run python ./gerrit-maintenance.py -d $SITE -h
+```
+
+At this level, the path to the Gerrit site has to be provided.
+
+The next layer deals with the different aspects of a Gerrit site:
+
+### Projects
+
+This set of subcommands deals with maintaining the projects/repositories in
+the Gerrit site. To get an overview of available commands, run:
+
+```sh
+pipenv run python ./gerrit-maintenance.py -d $SITE projects -h
+```
+
+By default the selected subcommand will run on all projects in the site, but the
+list can be filtered by either selecting projects specifically
+
+```sh
+pipenv run python ./gerrit-maintenance.py \
+ -d $SITE \
+ projects \
+ --project All-Users \
+ --project All-Projects \
+ $CMD
+```
+
+or by skipping some projects
+
+```sh
+pipenv run python ./gerrit-maintenance.py \
+ -d $SITE \
+ projects \
+ --skip All-Users \
+ --skip All-Projects \
+ $CMD
+```
+
+The maintenance scripts available for projects are:
+
+#### Git Garbage Collection
+
+To run Git GC as part of the gerrit-maintenance CLI, run:
+
+```sh
+pipenv run python ./gerrit-maintenance.py \
+ -d $SITE \
+ projects \
+ gc
+```
+
+You may run it as well as a [standalone git extension](#extended-git-garbagecollection).
+
+You can provide git configuration options to git gc using the `-c` option:
+
+```sh
+pipenv run python ./gerrit-maintenance.py \
+ -d $SITE \
+ projects \
+ gc \
+ -c repack.writebitmaps=false
+```
+
+As with the standalone git extension, all arguments provided in addition to the
+ones known by the CLI will be forwarded to the `git gc` command, e.g. the following
+command will suppress all progress reports logged by git:
+
+```sh
+pipenv run python ./gerrit-maintenance.py \
+ -d $SITE \
+ projects \
+ gc \
+ --quiet
+```
+
+The CLI also includes all extended features mentioned in [this section](#extended-features).
+
+## Extended Git GarbageCollection
+
+Git provides a GarbageCollection command (`git gc`) to clean up repositories.
+Unfortunately, this command misses some cleanup steps that help improving
+the performance of a repository.
+
+The python script provided here wraps `git gc` and adds additional options and
+cleanup steps.
+
+### Dependencies
+
+Refer to [general dependencies](#dependencies)
+
+No non-standard libraries are being used to keep running this tool simple.
+
+### Installation
+
+Put this directory somewhere convenient and ensure that the `git-gcplus`
+executable is present in the `PATH` environment variable, e.g. by symlinking it
+to `/usr/local/bin`.
+
+### Usage
+
+The extended git gc can be called like any other git-command:
+
+```sh
+git gcplus
+```
+
+This will run the extended gc in the current working directory (if it is a
+repository).
+
+A specific repository can be set as usual using `-C`:
+
+```sh
+git -C "/var/gerrit/git/All-Users.git" gcplus
+```
+
+The repository configuration can also be overridden as usual:
+
+```sh
+git -c repack.writebitmaps=false gcplus
+```
+
+The script will further forward all [options](https://git-scm.com/docs/git-gc#_options)
+provided by the `git gc` command to the included `git gc` run, e.g. the following
+command will suppress all progress reports written by git:
+
+```sh
+git gcplus --quiet
+```
+
+The extended git gc script also adds a few more options:
+
+- `--pack-all-refs` / `-r`
+
+### Extended features
+
+#### Packing all refs
+
+Enabled by: `--pack-all-refs` / `-r`
+
+Git gc by default only packs refs that are already packed. That potentially
+leaves a lot of loose refs in large projects, some of which are not actively
+being used anymore.
+
+Enabling this feature conveniently runs `git pack-refs --all`, if there are more
+than 10 loose refs after the `git-gc` run.
+
+#### Preserving packs
+
+Enabled by configuring `gc.preserveoldpacks = true`
+
+As part of git gc packs are rewritten, which includes the change of the pack names.
+If a long running request accesses a pack that is being recreated in this way
+while the request is running, the request can fail, because the server tries
+and fails to access the now deleted old pack. This can lead to a significant
+amount of failing requests on large repositories and greatly inconvenience users.
+
+Jgit provides a feature to prevent the above described scenario by allowing to
+preserve packs. This is done by hardlinking them before the gc and falling back
+to the preserved pack in case a request fails to find a pack. Unfortunately, this
+is not supported by native git.
+
+This extended gc script adds support for the following options added by jgit:
+
+- `gc.preserveoldpacks`: Whether to preserve packs before running `git gc`.
+- `gc.prunepreserved`: Whether to prune preserved packs created by previous runs.
+
+Setting those options will prevent failures as described above, if the server uses
+jgit (e.g. Gerrit), at a cost of using more storage.
+
+#### Lock handling
+
+Enabled: Always
+
+Git guards gc by locking a lock file "gc.pid" before starting execution.
+The lock file contains the pid and hostname of the process holding the
+lock. Git tries to kill the process holding that lock if the lock file
+wasn't modified in the last 12 hours and was started from the same host.
+
+This does not work in a scenario where git gc is running in an ephemeral
+environment like Kubernetes, where the host might actually always be different,
+e.g. if git gc is running in a Kubernetes CronJob on a repository in a shared
+filesystem.
+
+The extended git gc will always delete the lock, if it hasn't been modified for
+at least 12 h. This matches the behavior of jgit.
+
+#### Deletion of empty ref directories
+
+Enabled: Always
+
+Git gc might leave empty directories after packing refs. This happens if all refs
+in a namespace have been packed. This potentially leaves thousands of empty
+directories, especially with Gerrit's NoteDB. This can cause significant performance
+issues on slow filesystems like NFS.
+
+The extended gc will delete empty ref directories older than 1h.
+
+#### Deletion of stale incoming packs
+
+Enabled: Always
+
+If a git server crashes while still serving push requests the temporary incoming
+pack file will never be cleaned up, unnecessarily cluttering the repository.
+
+The extended gc will consider incoming packs not modified for 1 day to be stale
+and delete them.
+
+#### Using a marker file to enable aggressive gc
+
+Enabled by creating a file named `gc-aggressive` or `gc-aggressive-once` in the
+repository's `.git` directory.
+
+In some use cases an aggressive GC should be run for a while as part of a scheduled
+git gc. In that case it is not always convenient to change the calling script.
+
+The extended gc will check for the existence of the following files:
+
+- `gc-aggressive`
+- `gc-aggressive-once`
+
+In the latter case, the file will be deleted, effectively causing an aggressive
+gc just once.
diff --git a/contrib/maintenance/cli/__init__.py b/contrib/maintenance/cli/__init__.py
new file mode 100644
index 0000000..e61f878
--- /dev/null
+++ b/contrib/maintenance/cli/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (C) 2024 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.
diff --git a/contrib/maintenance/cli/gc.py b/contrib/maintenance/cli/gc.py
new file mode 100644
index 0000000..08456f9
--- /dev/null
+++ b/contrib/maintenance/cli/gc.py
@@ -0,0 +1,57 @@
+# Copyright (C) 2024 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.
+
+import sys
+
+sys.path.append("..")
+
+from git.gc import MAX_LOOSE_REF_COUNT
+
+
+PROG = "Execute Git Garbage Collection."
+DESCRIPTION = """
+Run Git GC with additional cleanup steps on repository.
+
+To specify a one-time --aggressive git gc for a repository X, simply
+create an empty file called 'gc-aggressive-once' in the `/path/to/X.git`
+folder:
+
+ $ cd /path/to/X.git
+ $ touch gc-aggressive-once
+
+On the next run, gc.sh will use --aggressive option for gc-ing this
+repository *and* will remove this file. Next time, gc.sh again runs
+normal gc for this repository.
+
+To specify a permanent --aggressive git gc for a repository, create
+an empty file named "gc-aggressive" in the same folder:
+
+ $ cd /path/to/X.git
+ $ touch gc-aggressive
+
+Every next git gc on this repository will use --aggressive option.
+"""
+
+
+def add_arguments(parser):
+ parser.add_argument(
+ "-r",
+ "--pack-all-refs",
+ help=(
+ "Whether to pack all refs, "
+ f"if more than {MAX_LOOSE_REF_COUNT} loose refs exist."
+ ),
+ dest="pack_refs",
+ action="store_true",
+ )
diff --git a/contrib/maintenance/gerrit-maintenance.py b/contrib/maintenance/gerrit-maintenance.py
new file mode 100755
index 0000000..5e3b7fa
--- /dev/null
+++ b/contrib/maintenance/gerrit-maintenance.py
@@ -0,0 +1,110 @@
+#!/usr/bin/python3
+
+# Copyright (C) 2024 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.
+
+import argparse
+import logging
+import sys
+
+import cli.gc
+
+from gerrit.site import Site
+from gerrit.tasks.gc import BatchGitGarbageGollection
+from git.gc import GitGarbageCollectionProvider
+
+logging.basicConfig(
+ level=logging.INFO,
+ stream=sys.stdout,
+ format="%(asctime)s [%(levelname)s] %(message)s",
+)
+
+
+def _run_projects_gc(args):
+ site = Site(args[0].site)
+ projects = (
+ args[0].projects
+ if args[0].projects
+ else site.get_projects(args[0].skip_projects)
+ )
+ BatchGitGarbageGollection(
+ site,
+ projects,
+ GitGarbageCollectionProvider.get(args[0].pack_refs, args[0].config),
+ ).run(args[1])
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "-d",
+ "--site",
+ help="Path to Gerrit site",
+ dest="site",
+ action="store",
+ default="/var/gerrit",
+ )
+ parser.set_defaults(func=lambda x: parser.print_usage())
+
+ subparsers = parser.add_subparsers()
+
+ parser_projects = subparsers.add_parser(
+ "projects",
+ help="Tools for working with Gerrit projects.",
+ )
+ parser_projects.add_argument(
+ "-p",
+ "--project",
+ help=(
+ "Which project to gc. Can be used multiple times. If not given, all "
+ "attrs=projects (except for `--skipped` ones) will be gc'ed."
+ ),
+ dest="projects",
+ action="append",
+ default=[],
+ )
+ parser_projects.add_argument(
+ "-s",
+ "--skip",
+ help="Which project to skip. Can be used multiple times.",
+ dest="skip_projects",
+ action="append",
+ default=[],
+ )
+ parser_projects.set_defaults(func=lambda x: parser_projects.print_usage())
+
+ subparsers_projects = parser_projects.add_subparsers()
+ parser_projects_gc = subparsers_projects.add_parser(
+ "gc",
+ prog=cli.gc.PROG,
+ description=cli.gc.DESCRIPTION,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ cli.gc.add_arguments(parser_projects_gc)
+ parser_projects_gc.add_argument(
+ "-c",
+ "--config",
+ help="Git config options to apply.",
+ dest="config",
+ action="append",
+ default=[],
+ )
+ parser_projects_gc.set_defaults(func=_run_projects_gc)
+
+ args = parser.parse_known_args()
+ args[0].func(args)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/contrib/maintenance/gerrit/__init__.py b/contrib/maintenance/gerrit/__init__.py
new file mode 100644
index 0000000..e61f878
--- /dev/null
+++ b/contrib/maintenance/gerrit/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (C) 2024 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.
diff --git a/contrib/maintenance/gerrit/site.py b/contrib/maintenance/gerrit/site.py
new file mode 100644
index 0000000..51d567f
--- /dev/null
+++ b/contrib/maintenance/gerrit/site.py
@@ -0,0 +1,56 @@
+# Copyright (C) 2024 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.
+
+import sys
+
+sys.path.append("..")
+
+import os
+
+from git.config import GitConfigReader
+from git.repo import GIT_SUFFIX
+
+
+class Site:
+ def __init__(self, path):
+ self.path = path
+ self.base_path = None
+
+ def get_etc_path(self):
+ return os.path.join(self.path, "etc")
+
+ def get_base_path(self):
+ if not self.base_path:
+ with GitConfigReader(
+ os.path.join(self.get_etc_path(), "gerrit.config")
+ ) as cfg:
+ config_base_path = cfg.get("gerrit", None, "basePath", "git")
+ if os.path.isabs(config_base_path):
+ self.basePath = config_base_path
+ else:
+ self.basePath = os.path.join(self.path, config_base_path)
+
+ return self.basePath
+
+ def get_projects(self, excludes=None):
+ for current, dirs, _ in os.walk(self.get_base_path(), topdown=True):
+ if os.path.splitext(current)[1] != GIT_SUFFIX:
+ continue
+
+ dirs.clear()
+ project = f"{current[len(self.get_base_path()) + 1:-len(GIT_SUFFIX)]}"
+ if excludes and project in excludes:
+ continue
+
+ yield project
diff --git a/contrib/maintenance/gerrit/tasks/__init__.py b/contrib/maintenance/gerrit/tasks/__init__.py
new file mode 100644
index 0000000..e61f878
--- /dev/null
+++ b/contrib/maintenance/gerrit/tasks/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (C) 2024 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.
diff --git a/contrib/maintenance/gerrit/tasks/gc.py b/contrib/maintenance/gerrit/tasks/gc.py
new file mode 100644
index 0000000..164a87b
--- /dev/null
+++ b/contrib/maintenance/gerrit/tasks/gc.py
@@ -0,0 +1,32 @@
+# Copyright (C) 2024 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.
+
+import os.path
+import sys
+
+sys.path.append("../..")
+
+from git.repo import GIT_SUFFIX
+
+
+class BatchGitGarbageGollection:
+ def __init__(self, site, projects, gc_runner):
+ self.site = site
+ self.projects = projects
+ self.gc_runner = gc_runner
+
+ def run(self, gc_args):
+ base_path = self.site.get_base_path()
+ for project in self.projects:
+ self.gc_runner.run(os.path.join(base_path, project + GIT_SUFFIX), gc_args)
diff --git a/contrib/maintenance/git-gcplus b/contrib/maintenance/git-gcplus
new file mode 100755
index 0000000..d9f6fcf
--- /dev/null
+++ b/contrib/maintenance/git-gcplus
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+
+# Copyright (C) 2024 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.
+
+import argparse
+import logging
+import sys
+
+import cli.gc
+
+from git.gc import GitGarbageCollectionProvider
+
+logging.basicConfig(
+ level=logging.INFO,
+ stream=sys.stderr,
+ format="%(asctime)s [%(levelname)s] %(message)s",
+)
+
+
+def _run_gc(args):
+ GitGarbageCollectionProvider.get(args[0].pack_refs).run(args=args[1])
+
+
+def main():
+ parser = argparse.ArgumentParser(
+ prog=cli.gc.PROG,
+ description=cli.gc.DESCRIPTION,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+
+ cli.gc.add_arguments(parser)
+
+ args = parser.parse_known_args()
+ _run_gc(args)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/contrib/maintenance/git/__init__.py b/contrib/maintenance/git/__init__.py
new file mode 100644
index 0000000..e61f878
--- /dev/null
+++ b/contrib/maintenance/git/__init__.py
@@ -0,0 +1,13 @@
+# Copyright (C) 2024 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.
diff --git a/contrib/maintenance/git/config.py b/contrib/maintenance/git/config.py
new file mode 100644
index 0000000..cdcb2b0
--- /dev/null
+++ b/contrib/maintenance/git/config.py
@@ -0,0 +1,217 @@
+# Copyright (C) 2024 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.
+
+import logging
+import re
+
+DEFAULT_SUBSECTION = "default"
+SECTION_HEADER_PATTERN = re.compile(
+ r"^\[(?P<section>[^\s\"]+)\s?\"?(?P<subsection>[^\s\"]+)?\"?\]$"
+)
+LOG = logging.getLogger(__name__)
+
+
+class GitConfigException(Exception):
+ """Exception thrown when git config could not be parsed."""
+
+
+class GitConfigReader:
+ def __init__(self, config_path):
+ self.path = config_path
+ self.contents = {}
+
+ def __enter__(self):
+ LOG.debug("reader")
+ self.file = open(self.path, "r", encoding="utf-8")
+ self._parse()
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.file.close()
+ self.contents = {}
+
+ def get(self, section, subsection, key, default=None, all=False):
+ if not subsection:
+ subsection = DEFAULT_SUBSECTION
+ if (
+ section not in self.contents
+ or subsection not in self.contents[section]
+ or key not in self.contents[section][subsection]
+ ):
+ return default
+ value = self.contents[section][subsection][key]
+ if isinstance(value, list) and not all:
+ return value[-1]
+ return value
+
+ def list(self):
+ return self.contents
+
+ def _parse(self):
+ current_section = None
+ current_subsection = None
+ for line in self.file.readlines():
+ LOG.debug("Read config line: %s", line)
+ line = line.split("#", 1)[0].strip()
+ if not line:
+ continue
+ LOG.debug("Not a comment: |%s|", line)
+ section_match = SECTION_HEADER_PATTERN.match(line)
+ if section_match:
+ LOG.debug(section_match.groupdict())
+ current_section = section_match.group("section").lower()
+ if not current_section:
+ raise GitConfigException("Section has to be set with subsection.")
+ LOG.debug("Parsed section %s", current_section)
+ current_subsection = section_match.group("subsection")
+ if not current_subsection:
+ current_subsection = DEFAULT_SUBSECTION
+ current_subsection = current_subsection.lower()
+ LOG.debug("Parsed subsection %s", current_subsection)
+ else:
+ LOG.debug("Parsing key-value pair: |%s|", line)
+ key, value = line.split("=", 1)
+ key = key.strip().lower()
+ value = value.strip()
+ if value.lower() == "true":
+ value = True
+ elif value.lower() == "false":
+ value = False
+ if not current_section:
+ raise GitConfigException(
+ "All key-value pairs have to be part of a section."
+ )
+ self._ensure_full_section(current_section, current_subsection)
+ if key not in self.contents[current_section][current_subsection]:
+ self.contents[current_section][current_subsection][key] = value
+ else:
+ if isinstance(
+ self.contents[current_section][current_subsection][key], list
+ ):
+ self.contents[current_section][current_subsection][key].append(
+ value
+ )
+ else:
+ self.contents[current_section][current_subsection][key] = [
+ self.contents[current_section][current_subsection][key],
+ value,
+ ]
+ LOG.debug("Parsed config: %s", self.contents)
+
+ def _ensure_full_section(self, section, subsection):
+ if section not in self.contents:
+ self.contents[section] = {}
+ if subsection not in self.contents[section]:
+ self.contents[section][subsection] = {}
+
+
+class GitConfigWriter(GitConfigReader):
+ def __init__(self, config_path):
+ super().__init__(config_path)
+
+ def __enter__(self):
+ self.file = open(self.path, "r+", encoding="utf-8")
+ self._parse()
+ return self
+
+ def set(self, section, subsection, key, value):
+ section, subsection, key = self._ensure_full_key_format(
+ section, subsection, key
+ )
+ self._ensure_full_section(section, subsection)
+ self.contents[section][subsection][key] = value
+
+ def add(self, section, subsection, key, value):
+ section, subsection, key = self._ensure_full_key_format(
+ section, subsection, key
+ )
+ self._ensure_full_section(section, subsection)
+ if key not in self.contents[section][subsection]:
+ self.contents[section][subsection][key] = value
+ if isinstance(self.contents[section][subsection][key], list):
+ self.contents[section][subsection][key].append(value)
+ else:
+ self.contents[section][subsection][key] = [
+ self.contents[section][subsection][key],
+ value,
+ ]
+
+ def unset(self, section, subsection, key):
+ section, subsection, key = self._ensure_full_key_format(
+ section, subsection, key
+ )
+ if section not in self.contents or subsection not in self.contents[section]:
+ return
+ self.contents[section][subsection].pop(key, None)
+ if not self.contents[section][subsection]:
+ self.contents[section].pop(subsection, None)
+ if not self.contents[section]:
+ self.contents.pop(section, None)
+
+ def remove(self, section, subsection, key, value):
+ section, subsection, key = self._ensure_full_key_format(
+ section, subsection, key
+ )
+ if (
+ section not in self.contents
+ or subsection not in self.contents[section]
+ or key not in self.contents[section][subsection]
+ ):
+ return
+ try:
+ self.contents[section][subsection][key].remove(value)
+ except ValueError:
+ return
+
+ def write(self):
+ formatted = ""
+ for section in self.contents:
+ for subsection in self.contents[section]:
+ if subsection == DEFAULT_SUBSECTION:
+ formatted += self._format_section(
+ section, None, self.contents[section][subsection]
+ )
+ else:
+ formatted += self._format_section(
+ section, subsection, self.contents[section][subsection]
+ )
+ LOG.debug("Writing config:\n %s \n\n to %s", formatted, self.file)
+ self.file.truncate(0)
+ self.file.seek(0)
+ self.file.write(formatted)
+
+ def _format_section(self, section, subsection, entries):
+ if subsection:
+ formatted = f'[{section} "{subsection}"]\n'
+ else:
+ formatted = f"[{section}]\n"
+
+ for key, value in entries.items():
+ if isinstance(value, list):
+ for v in value:
+ formatted += f" {key} = {v}\n"
+ else:
+ formatted += f" {key} = {value}\n"
+
+ return formatted
+
+ def _ensure_full_key_format(self, section, subsection, key):
+ if not subsection:
+ subsection = DEFAULT_SUBSECTION
+
+ section = section.lower()
+ subsection = subsection.lower()
+ key = key.lower()
+
+ return section, subsection, key
diff --git a/contrib/maintenance/git/gc.py b/contrib/maintenance/git/gc.py
new file mode 100644
index 0000000..7a4c087
--- /dev/null
+++ b/contrib/maintenance/git/gc.py
@@ -0,0 +1,220 @@
+# Copyright (C) 2024 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.
+
+import abc
+import logging
+import os
+
+from datetime import datetime, timedelta
+from glob import glob
+
+from .config import GitConfigReader
+from . import repo
+
+LOG = logging.getLogger(__name__)
+
+AGGRESSIVE_FLAG = "--aggressive"
+MAX_AGE_GC_LOCK = timedelta(hours=12)
+MAX_AGE_EMPTY_REF_DIRS = timedelta(hours=1)
+MAX_AGE_INCOMING_PACKS = timedelta(days=1)
+MAX_LOOSE_REF_COUNT = 10
+PACK_PATH = "objects/pack"
+PRESERVED_PACK_PATH = f"{PACK_PATH}/preserved"
+
+
+class Util:
+ @staticmethod
+ def is_file_stale(file, max_age):
+ return datetime.fromtimestamp(os.stat(file).st_mtime) + max_age < datetime.now()
+
+
+class GCStep(abc.ABC):
+ @abc.abstractmethod
+ def run(self, repo_dir):
+ pass
+
+
+class GCLockHandlingInitStep(GCStep):
+ def run(self, repo_dir):
+ gc_lock_path = os.path.join(repo_dir, "gc.pid")
+ if os.path.exists(gc_lock_path) and Util.is_file_stale(
+ gc_lock_path, MAX_AGE_GC_LOCK
+ ):
+ LOG.warning(
+ "Pruning stale 'gc.pid' lock file older than %s min: %s",
+ MAX_AGE_GC_LOCK.min,
+ gc_lock_path,
+ )
+ os.remove(gc_lock_path)
+
+
+class PreservePacksInitStep(GCStep):
+ def run(self, repo_dir):
+ with GitConfigReader(os.path.join(repo_dir, "config")) as config_reader:
+ is_prune_preserved = config_reader.get("gc", None, "prunepreserved", False)
+ is_preserve_old_packs = config_reader.get(
+ "gc", None, "preserveoldpacks", False
+ )
+
+ if is_prune_preserved:
+ self._prune_preserved(repo_dir)
+
+ if is_preserve_old_packs:
+ self._preserve_packs(repo_dir)
+
+ def _prune_preserved(self, repo_dir):
+ full_preserved_pack_path = os.path.join(repo_dir, PRESERVED_PACK_PATH)
+ if os.path.exists(full_preserved_pack_path):
+ LOG.info("Pruning old preserved packs.")
+ count = 0
+ for file in os.listdir(full_preserved_pack_path):
+ if file.endswith(".old-pack") or file.endswith(".old-idx"):
+ count += 1
+ full_old_pack_path = os.path.join(full_preserved_pack_path, file)
+ LOG.debug("Deleting %s", full_old_pack_path)
+ os.remove(full_old_pack_path)
+ LOG.info("Done pruning %d old preserved packs.", count)
+
+ def _preserve_packs(self, repo_dir):
+ full_pack_path = os.path.join(repo_dir, PACK_PATH)
+ full_preserved_pack_path = os.path.join(repo_dir, PRESERVED_PACK_PATH)
+ if not os.path.exists(full_preserved_pack_path):
+ os.makedirs(full_preserved_pack_path)
+ LOG.info("Preserving packs.")
+ count = 0
+ for file in os.listdir(full_pack_path):
+ full_file_path = os.path.join(full_pack_path, file)
+ filename, ext = os.path.splitext(file)
+ if (
+ os.path.isfile(full_file_path)
+ and filename.startswith("pack-")
+ and ext in [".pack", ".idx"]
+ ):
+ LOG.debug("Preserving pack %s", file)
+ os.link(
+ os.path.join(full_pack_path, file),
+ os.path.join(
+ full_preserved_pack_path,
+ self._get_preserved_packfile_name(file),
+ ),
+ )
+ if ext == ".pack":
+ count += 1
+ LOG.info("Preserved %d packs", count)
+
+ def _get_preserved_packfile_name(self, file):
+ filename, ext = os.path.splitext(file)
+ return f"{filename}.old-{ext[1:]}"
+
+
+DEFAULT_INIT_STEPS = [GCLockHandlingInitStep(), PreservePacksInitStep()]
+
+
+class DeleteEmptyRefDirsCleanupStep(GCStep):
+ def run(self, repo_dir):
+ refs_path = os.path.join(repo_dir, "refs")
+ for dir in glob(os.path.join(refs_path, "*/*")):
+ if (
+ os.path.isdir(dir)
+ and len(os.listdir(dir)) == 0
+ and Util.is_file_stale(dir, MAX_AGE_EMPTY_REF_DIRS)
+ ):
+ os.removedirs(dir)
+
+
+class DeleteStaleIncomingPacksCleanupStep(GCStep):
+ def run(self, repo_dir):
+ objects_path = os.path.join(repo_dir, "objects")
+ for file in glob(os.path.join(objects_path, "incoming_*.pack")):
+ if Util.is_file_stale(file, MAX_AGE_INCOMING_PACKS):
+ LOG.warning(
+ "Pruning stale incoming pack/index file older than %d days: %s",
+ MAX_AGE_INCOMING_PACKS.days,
+ file,
+ )
+ os.remove(file)
+
+
+class PackAllRefsAfterStep(GCStep):
+ def run(self, repo_dir):
+ loose_ref_count = 0
+ for _, _, files in os.walk(os.path.join(repo_dir, "refs"), topdown=True):
+ loose_ref_count += len([file for file in files])
+ if loose_ref_count > MAX_LOOSE_REF_COUNT:
+ repo.pack_refs(repo_dir, all=True)
+ LOG.info("Found %d loose refs -> pack all refs", loose_ref_count)
+ else:
+ LOG.info(
+ "Found less than %d refs -> skipping pack all refs"
+ % MAX_LOOSE_REF_COUNT
+ )
+
+
+DEFAULT_AFTER_STEPS = [
+ DeleteEmptyRefDirsCleanupStep(),
+ DeleteStaleIncomingPacksCleanupStep(),
+]
+
+
+class GitGarbageCollectionProvider:
+ @staticmethod
+ def get(pack_refs=True, git_config=None):
+ init_steps = DEFAULT_INIT_STEPS.copy()
+ after_steps = DEFAULT_AFTER_STEPS.copy()
+
+ if pack_refs:
+ after_steps.append(PackAllRefsAfterStep())
+
+ return GitGarbageCollection(init_steps, after_steps, git_config)
+
+
+class GitGarbageCollection:
+ def __init__(self, init_steps, after_steps, git_config=None):
+ self.init_steps = init_steps
+ self.after_steps = after_steps
+ self.git_config = git_config
+
+ def run(self, repo_dir=None, args=None):
+ LOG.info("Started gc in %s", repo_dir)
+ if not repo_dir:
+ repo_dir = repo.git_dir()
+ if not os.path.exists(repo_dir) or not os.path.isdir(repo_dir):
+ LOG.error("Failed: Directory does not exist: %s", repo_dir)
+ return
+
+ for init_step in self.init_steps:
+ init_step.run(repo_dir)
+
+ if self._is_aggressive(repo_dir) and AGGRESSIVE_FLAG not in args:
+ args.append(AGGRESSIVE_FLAG)
+
+ try:
+ repo.gc(repo_dir, self.git_config, args)
+ except repo.GitCommandException:
+ LOG.error("Failed to run gc in %s", repo_dir)
+
+ for after_step in self.after_steps:
+ after_step.run(repo_dir)
+
+ LOG.info("Finished gc in %s", repo_dir)
+
+ def _is_aggressive(self, project_dir):
+ if os.path.exists(os.path.join(project_dir, "gc-aggressive")):
+ LOG.info("Running aggressive gc in %s", project_dir)
+ return True
+ elif os.path.exists(os.path.join(project_dir, "gc-aggressive-once")):
+ LOG.info("Running aggressive gc once in %s", project_dir)
+ os.remove(os.path.join(project_dir, "gc-aggressive-once"))
+ return True
+ return False
diff --git a/contrib/maintenance/git/repo.py b/contrib/maintenance/git/repo.py
new file mode 100644
index 0000000..12fb1da
--- /dev/null
+++ b/contrib/maintenance/git/repo.py
@@ -0,0 +1,135 @@
+# 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.
+
+import logging
+import os
+import subprocess
+
+LOG = logging.getLogger(__name__)
+
+GIT_SUFFIX = ".git"
+
+
+class GitCommandException(Exception):
+ """Exception thrown by failed git commands."""
+
+
+def git_dir():
+ try:
+ return (
+ subprocess.run(
+ ["git", "rev-parse", "--git-dir"], capture_output=True, check=True
+ )
+ .stdout.decode()
+ .strip()
+ )
+ except subprocess.CalledProcessError:
+ raise GitCommandException("Unable to find .git directory.")
+
+
+def commit_id(repo_dir, ref="HEAD"):
+ try:
+ cmd = ["git", "rev-parse", "--short", ref]
+ return (
+ subprocess.run(cmd, cwd=repo_dir, capture_output=True, check=True)
+ .stdout.decode()
+ .strip()
+ )
+ except subprocess.CalledProcessError:
+ raise GitCommandException("Unable to parse current commit ID.")
+
+
+def add(repo_dir, files=None):
+ if not files:
+ files = ["."]
+ try:
+ cmd = ["git", "add"]
+ cmd.extend(files)
+ subprocess.run(cmd, cwd=repo_dir, capture_output=True, check=True)
+ except subprocess.CalledProcessError:
+ raise GitCommandException("Unable to add files to index.")
+
+
+def commit(repo_dir, message):
+ try:
+ cmd = ["git", "commit", "-m", message]
+ subprocess.run(cmd, cwd=repo_dir, capture_output=True, check=True)
+ except subprocess.CalledProcessError:
+ raise GitCommandException("Unable to commit.")
+
+
+def push(repo_dir, remote, refspec):
+ try:
+ cmd = ["git", "push", remote, refspec]
+ subprocess.run(cmd, cwd=repo_dir, capture_output=True, check=True)
+ except subprocess.CalledProcessError:
+ raise GitCommandException("Unable to push.")
+
+
+def clone(url, target_dir=""):
+ try:
+ cmd = ["git", "clone", url, target_dir]
+ subprocess.run(cmd, capture_output=True, check=True)
+ if target_dir:
+ return target_dir
+
+ repo_name = url.split("/")[-1]
+ if repo_name.endswith(GIT_SUFFIX):
+ repo_name = repo_name[: -len(GIT_SUFFIX)]
+ return repo_name
+ except subprocess.CalledProcessError:
+ raise GitCommandException(f"Unable to clone repo {url}.")
+
+
+def init(base_dir, repo_name, bare=False):
+ try:
+ cmd = ["git", "init"]
+ if bare:
+ cmd.append("--bare")
+ cmd.append(os.path.join(base_dir, repo_name))
+ subprocess.run(cmd, cwd=base_dir, capture_output=True, check=True)
+ except subprocess.CalledProcessError:
+ raise GitCommandException(f"Unable to initialize git repo {repo_name}.")
+
+
+def pack_refs(repo_dir, all=False):
+ command = "git pack-refs"
+ if all:
+ command += " --all"
+ try:
+ subprocess.run(command, cwd=repo_dir, shell=True, check=True)
+ except subprocess.CalledProcessError as e:
+ if e.stdout:
+ LOG.info(e.stdout)
+ if e.stderr:
+ LOG.error(e.stderr)
+ raise GitCommandException(f"Failed to pack refs in {repo_dir}")
+
+
+def gc(repo_dir, git_config=None, args=None):
+ cmd = "git "
+ if git_config:
+ cmd = cmd + "-c " + " -c ".join(git_config)
+ cmd += " gc"
+ if args:
+ cmd = cmd + " " + " ".join(args)
+ try:
+ # Git gc requires a shell to output logs, i.e. `shell` has to be `True`
+ subprocess.run(cmd, cwd=repo_dir, shell=True, check=True)
+ except subprocess.CalledProcessError as e:
+ if e.stdout:
+ LOG.info(e.stdout)
+ if e.stderr:
+ LOG.error(e.stderr)
+ raise GitCommandException(f"Failed to run gc in {repo_dir}")
diff --git a/contrib/maintenance/pyproject.toml b/contrib/maintenance/pyproject.toml
new file mode 100644
index 0000000..9cde8b2
--- /dev/null
+++ b/contrib/maintenance/pyproject.toml
@@ -0,0 +1,12 @@
+[project]
+name = "gerrit-maintenance"
+description = "Suite of tools to maintain a Gerrit site"
+
+[tool.pytest.ini_options]
+addopts = [
+ "--import-mode=importlib",
+]
+log_cli = "True"
+log_cli_level = "DEBUG"
+pythonpath = "."
+testpaths = ["tests"]
diff --git a/contrib/maintenance/tests/gerrit/test_site.py b/contrib/maintenance/tests/gerrit/test_site.py
new file mode 100644
index 0000000..4381983
--- /dev/null
+++ b/contrib/maintenance/tests/gerrit/test_site.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2024 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.
+
+import os
+import pytest
+
+from gerrit.site import Site
+from git.repo import init, GIT_SUFFIX
+
+REPOSITORIES = ["All-Projects", "All-Users", "test", "nested/repo"]
+
+
+@pytest.fixture(scope="function")
+def site(tmp_path_factory):
+ site = tmp_path_factory.mktemp("site")
+ base_path = os.path.join(site, "git")
+ os.makedirs(base_path)
+ for repo in REPOSITORIES:
+ init(base_path, repo + GIT_SUFFIX, bare=True)
+ etc_path = os.path.join(site, "etc")
+ os.makedirs(etc_path)
+ with open(os.path.join(etc_path, "gerrit.config"), "w") as f:
+ f.write(
+ """
+ [gerrit]
+ basePath = git
+ """
+ )
+ return site
+
+
+def test_get_projects(site):
+ site = Site(site)
+ assert REPOSITORIES.sort() == list(site.get_projects()).sort()
+ assert "test" not in list(site.get_projects(excludes=["test"]))
diff --git a/contrib/maintenance/tests/git/conftest.py b/contrib/maintenance/tests/git/conftest.py
new file mode 100644
index 0000000..474546b
--- /dev/null
+++ b/contrib/maintenance/tests/git/conftest.py
@@ -0,0 +1,33 @@
+# Copyright (C) 2024 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.
+
+import os.path
+import pytest
+
+import git.repo
+
+
+@pytest.fixture(scope="function")
+def repo(tmp_path_factory):
+ dir = tmp_path_factory.mktemp("repos")
+ repo_name = "test.git"
+ git.repo.init(dir, repo_name, bare=True)
+ return os.path.join(dir, repo_name)
+
+
+@pytest.fixture(scope="function")
+def local_repo(tmp_path_factory, repo):
+ dir = tmp_path_factory.mktemp("local.git")
+ git.repo.clone(repo, dir)
+ return dir
diff --git a/contrib/maintenance/tests/git/test_config.py b/contrib/maintenance/tests/git/test_config.py
new file mode 100644
index 0000000..f8039d6
--- /dev/null
+++ b/contrib/maintenance/tests/git/test_config.py
@@ -0,0 +1,122 @@
+# Copyright (C) 2024 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.
+
+import os.path
+import pytest
+
+from git.config import DEFAULT_SUBSECTION, GitConfigReader, GitConfigWriter
+
+CONFIG = """
+[section]
+ key = value
+[section "subsection"]
+ other_key = test
+ other_key = another_value
+ another_key = test
+[another_section]
+ # some comment
+ gerrit = awesome # of course
+ boolean = true
+ another_boolean = false
+"""
+
+CONFIG_DICT = {
+ "section": {
+ "default": {"key": "value"},
+ "subsection": {"other_key": ["test", "another_value"], "another_key": "test"},
+ },
+ "another_section": {
+ "default": {"gerrit": "awesome", "boolean": True, "another_boolean": False}
+ },
+}
+
+
+@pytest.fixture(scope="function")
+def repo_with_config(repo):
+ with open(os.path.join(repo, "config"), "w") as f:
+ f.write(CONFIG)
+ return repo
+
+
+def test_list_config(repo_with_config):
+ with GitConfigReader(os.path.join(repo_with_config, "config")) as reader:
+ assert reader.list() == CONFIG_DICT
+
+
+def test_get_config(repo_with_config):
+ with GitConfigReader(os.path.join(repo_with_config, "config")) as reader:
+ assert (
+ reader.get("section", None, "key")
+ == CONFIG_DICT["section"]["default"]["key"]
+ )
+ assert (
+ reader.get("section", "subsection", "another_key")
+ == CONFIG_DICT["section"]["subsection"]["another_key"]
+ )
+ assert (
+ reader.get("section", "subsection", "other_key")
+ == CONFIG_DICT["section"]["subsection"]["other_key"][-1]
+ )
+ assert (
+ reader.get("section", "subsection", "other_key", all=True)
+ == CONFIG_DICT["section"]["subsection"]["other_key"]
+ )
+ assert reader.get("another_section", "default", "boolean")
+ assert not reader.get("another_section", "default", "another_boolean")
+
+
+def test_set_config(repo_with_config):
+ with GitConfigWriter(os.path.join(repo_with_config, "config")) as writer:
+ writer.set("new", None, "key", "value")
+ writer.set("new", "new_sub", "key", "val")
+ writer.write()
+
+ with GitConfigReader(os.path.join(repo_with_config, "config")) as reader:
+ assert reader.get("new", None, "key") == "value"
+ assert reader.get("new", "new_sub", "key") == "val"
+
+
+def test_add_to_config(repo_with_config):
+ with GitConfigWriter(os.path.join(repo_with_config, "config")) as writer:
+ writer.add("new", None, "key", "value")
+ writer.add("section", None, "key", "value2")
+ writer.add("section", "subsection", "other_key", "val")
+ writer.write()
+
+ with GitConfigReader(os.path.join(repo_with_config, "config")) as reader:
+ assert reader.get("new", None, "key") == "value"
+ assert reader.get("section", None, "key") == "value2"
+ assert reader.get("section", "subsection", "other_key") == "val"
+
+
+def test_unset_config(repo_with_config):
+ with GitConfigWriter(os.path.join(repo_with_config, "config")) as writer:
+ writer.unset("section", None, "key")
+ writer.unset("section", "subsection", "another_key")
+ writer.write()
+
+ with GitConfigReader(os.path.join(repo_with_config, "config")) as reader:
+ config = reader.list()
+ assert DEFAULT_SUBSECTION not in config["section"]
+ assert "another_key" not in config["section"]["subsection"]
+
+
+def test_remove_config(repo_with_config):
+ with GitConfigWriter(os.path.join(repo_with_config, "config")) as writer:
+ writer.remove("section", "subsection", "other_key", "test")
+ writer.write()
+
+ with GitConfigReader(os.path.join(repo_with_config, "config")) as reader:
+ config = reader.list()
+ assert "test" not in config["section"]["subsection"]["other_key"]
diff --git a/contrib/maintenance/tests/git/test_gc.py b/contrib/maintenance/tests/git/test_gc.py
new file mode 100644
index 0000000..a7d831f
--- /dev/null
+++ b/contrib/maintenance/tests/git/test_gc.py
@@ -0,0 +1,193 @@
+# Copyright (C) 2024 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.
+
+import os
+import unittest.mock as mock
+
+import git.repo
+
+from datetime import datetime, timedelta
+from pathlib import Path
+from git.gc import (
+ DeleteStaleIncomingPacksCleanupStep,
+ DeleteEmptyRefDirsCleanupStep,
+ GCLockHandlingInitStep,
+ GitGarbageCollection,
+ PackAllRefsAfterStep,
+ PreservePacksInitStep,
+)
+from git.config import GitConfigWriter
+
+
+def test_GCLockHandlingInitStep(repo):
+ lock_file = os.path.join(repo, "gc.pid")
+ with open(lock_file, "w") as f:
+ f.write("1234")
+
+ task = GCLockHandlingInitStep()
+
+ task.run(repo)
+ assert os.path.exists(lock_file)
+
+ _mofify_last_modified(lock_file, timedelta(hours=13))
+
+ task.run(repo)
+ assert not os.path.exists(lock_file)
+
+
+def test_PreservePacksInitStep(repo):
+ task = PreservePacksInitStep()
+
+ pack_path = os.path.join(repo, "objects", "pack")
+ preserved_pack_path = os.path.join(pack_path, "preserved")
+
+ fake_pack = os.path.join(pack_path, "pack-fake.pack")
+ fake_preserved_pack = os.path.join(preserved_pack_path, "pack-fake.old-pack")
+ fake_idx = os.path.join(pack_path, "pack-fake.idx")
+ fake_preserved_idx = os.path.join(preserved_pack_path, "pack-fake.old-idx")
+ fake_rev = os.path.join(pack_path, "pack-fake.rev")
+ fake_preserved_rev = os.path.join(preserved_pack_path, "pack-fake.old-rev")
+
+ Path(fake_pack).touch()
+ Path(fake_idx).touch()
+ Path(fake_rev).touch()
+
+ with GitConfigWriter(os.path.join(repo, "config")) as writer:
+ writer.set("gc", None, "preserveoldpacks", False)
+ writer.write()
+
+ task.run(repo)
+
+ assert not os.path.exists(fake_preserved_pack)
+ assert not os.path.exists(fake_preserved_idx)
+ assert not os.path.exists(fake_preserved_rev)
+
+ with GitConfigWriter(os.path.join(repo, "config")) as writer:
+ writer.set("gc", None, "preserveoldpacks", True)
+ writer.write()
+
+ task.run(repo)
+
+ assert os.path.exists(fake_preserved_pack)
+ assert os.path.exists(fake_preserved_idx)
+ assert not os.path.exists(fake_preserved_rev)
+
+ with GitConfigWriter(os.path.join(repo, "config")) as writer:
+ writer.set("gc", None, "preserveoldpacks", False)
+ writer.set("gc", None, "prunepreserved", True)
+ writer.write()
+
+ task.run(repo)
+
+ assert not os.path.exists(fake_preserved_pack)
+ assert not os.path.exists(fake_preserved_idx)
+
+
+def test_DeleteEmptyRefDirsCleanupStep(repo):
+ delete_path = os.path.join(repo, "refs", "heads", "delete")
+ os.makedirs(delete_path)
+ keep_path = os.path.join(repo, "refs", "heads", "keep")
+ os.makedirs(keep_path)
+ Path(os.path.join(keep_path, "abcd1234")).touch()
+
+ task = DeleteEmptyRefDirsCleanupStep()
+
+ task.run(repo)
+ assert os.path.exists(delete_path)
+ assert os.path.exists(keep_path)
+
+ _mofify_last_modified(delete_path, timedelta(hours=2))
+ task.run(repo)
+ assert not os.path.exists(delete_path)
+
+
+def test_DeleteStaleIncomingPacksCleanupStep(repo):
+ task = DeleteStaleIncomingPacksCleanupStep()
+
+ objects_path = os.path.join(repo, "objects")
+ pack_path = os.path.join(objects_path, "pack")
+ pack_file = os.path.join(pack_path, "pack-1234.pack")
+ Path(pack_file).touch()
+ object_shard = os.path.join(objects_path, "f8")
+ os.makedirs(object_shard)
+ object_file = os.path.join(objects_path, "f8", "abcd")
+ Path(object_file).touch()
+ incoming_pack_file = os.path.join(objects_path, "incoming_1234.pack")
+ Path(incoming_pack_file).touch()
+
+ task.run(repo)
+
+ assert os.path.exists(pack_file)
+ assert os.path.exists(object_file)
+ assert os.path.exists(incoming_pack_file)
+
+ _mofify_last_modified(pack_file, timedelta(days=2))
+ _mofify_last_modified(object_file, timedelta(days=2))
+ _mofify_last_modified(incoming_pack_file, timedelta(days=2))
+
+ task.run(repo)
+
+ assert os.path.exists(pack_file)
+ assert os.path.exists(object_file)
+ assert not os.path.exists(incoming_pack_file)
+
+
+def test_PackAllRefsAfterStep(repo, local_repo):
+ test_file = Path(os.path.join(local_repo, "test.txt"))
+ test_file.touch()
+ git.repo.add(local_repo, [test_file])
+ git.repo.commit(local_repo, "test commit")
+
+ target_loose_ref_count = 15
+ loose_ref_count = 0
+ while loose_ref_count < target_loose_ref_count:
+ loose_ref_count += 1
+ git.repo.push(local_repo, "origin", f"HEAD:refs/heads/test{loose_ref_count}")
+
+ task = PackAllRefsAfterStep()
+ task.run(repo)
+
+ assert len(os.listdir(os.path.join(repo, "refs", "heads"))) == 0
+ packed_refs_file = os.path.join(repo, "packed-refs")
+ assert os.path.exists(packed_refs_file)
+ with open(packed_refs_file, "r") as f:
+ assert (
+ len(f.readlines()) == target_loose_ref_count + 1
+ ) # First line is a comment
+
+ git.repo.push(
+ local_repo, "origin", f"HEAD:refs/heads/test{target_loose_ref_count + 1}"
+ )
+ task.run(repo)
+ assert len(os.listdir(os.path.join(repo, "refs", "heads"))) == 1
+ with open(packed_refs_file, "r") as f:
+ assert (
+ len(f.readlines()) == target_loose_ref_count + 1
+ ) # First line is a comment
+
+
+@mock.patch("subprocess.run")
+def test_gc_executed(mock_subproc_run, repo):
+ gc = GitGarbageCollection([], [])
+ gc.run(repo)
+ mock_subproc_run.assert_called()
+ assert mock_subproc_run.call_count == 1
+
+
+def _mofify_last_modified(file, time_delta):
+ file_stat = os.stat(file)
+ new_mod_timestamp = datetime.timestamp(
+ datetime.fromtimestamp(file_stat.st_mtime) - time_delta
+ )
+ os.utime(file, (file_stat.st_atime, new_mod_timestamp))
diff --git a/contrib/maintenance/tests/git/test_repo.py b/contrib/maintenance/tests/git/test_repo.py
new file mode 100644
index 0000000..cb2e8b7
--- /dev/null
+++ b/contrib/maintenance/tests/git/test_repo.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2024 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.
+
+import os.path
+import pytest
+
+import git.repo
+
+
+@pytest.fixture(scope="function")
+def tmp_dir(tmp_path_factory):
+ return tmp_path_factory.mktemp("ltmp_dir")
+
+
+def test_git_init_commit(tmp_dir):
+ repo_name = "repo"
+ repo_path = os.path.join(tmp_dir, repo_name)
+ repo_git_path = os.path.join(repo_path, ".git")
+
+ git.repo.init(tmp_dir, repo_name)
+ assert os.path.exists(repo_path)
+ assert os.path.exists(repo_git_path)
+
+ new_commit = _create_new_commit(repo_path)
+ assert new_commit
+
+
+def test_git_clone_push(tmp_dir, repo):
+ repo_name = "repo"
+ repo_path = os.path.join(tmp_dir, repo_name)
+ repo_git_path = os.path.join(repo_path, ".git")
+
+ git.repo.clone(repo, repo_path)
+ assert os.path.exists(repo_path)
+ assert os.path.exists(repo_git_path)
+
+ new_commit = _create_new_commit(repo_path)
+ assert new_commit
+
+ git.repo.push(repo_path, "origin", "HEAD:refs/heads/master")
+ assert git.repo.commit_id(repo, "refs/heads/master") == new_commit
+
+
+def _create_new_commit(repo_path):
+ with open(os.path.join(repo_path, "test.txt"), "w") as f:
+ f.write("test content")
+
+ git.repo.add(repo_path)
+ git.repo.commit(repo_path, "Test commit")
+ return git.repo.commit_id(repo_path)
diff --git a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
index 4748e31..a5757ef 100644
--- a/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
+++ b/java/com/google/gerrit/acceptance/DisabledChangeIndex.java
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.DataSource;
@@ -72,6 +73,11 @@
}
@Override
+ public void deleteAllForProject(Project.NameKey project) {
+ throw new UnsupportedOperationException("ChangeIndex is disabled");
+ }
+
+ @Override
public void deleteAll() {
throw new UnsupportedOperationException("ChangeIndex is disabled");
}
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index 5a1de63..7ef8fed 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -45,6 +45,7 @@
import com.google.gerrit.extensions.webui.ResolveConflictsWebLink;
import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.ServerStateProvider;
+import com.google.gerrit.server.ValidationOptionsListener;
import com.google.gerrit.server.account.AccountStateProvider;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.change.FilterIncludedIn;
@@ -52,10 +53,12 @@
import com.google.gerrit.server.config.ProjectConfigEntry;
import com.google.gerrit.server.git.ChangeMessageModifier;
import com.google.gerrit.server.git.receive.PluginPushOption;
+import com.google.gerrit.server.git.validators.CommitValidationInfoListener;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
import com.google.gerrit.server.git.validators.RefOperationValidationListener;
import com.google.gerrit.server.logging.PerformanceLogger;
+import com.google.gerrit.server.query.approval.ApprovalQueryBuilder.UserInOperandFactory;
import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeHasOperandFactory;
import com.google.gerrit.server.query.change.ChangeQueryBuilder.ChangeIsOperandFactory;
import com.google.gerrit.server.restapi.change.OnPostReview;
@@ -111,9 +114,12 @@
private final DynamicSet<ServerStateProvider> serverStateProviders;
private final DynamicSet<AccountStateProvider> accountStateProviders;
private final DynamicSet<AttentionSetListener> attentionSetListeners;
+ private final DynamicSet<ValidationOptionsListener> validationOptionsListeners;
+ private final DynamicSet<CommitValidationInfoListener> commitValidationInfoListeners;
private final DynamicMap<ChangeHasOperandFactory> hasOperands;
private final DynamicMap<ChangeIsOperandFactory> isOperands;
+ private final DynamicMap<UserInOperandFactory> userInOperands;
private final DynamicMap<ReviewerSuggestion> reviewerSuggestions;
@Inject
@@ -157,9 +163,12 @@
DynamicSet<ReviewerDeletedListener> reviewerDeletedListeners,
DynamicMap<ChangeHasOperandFactory> hasOperands,
DynamicMap<ChangeIsOperandFactory> isOperands,
+ DynamicMap<UserInOperandFactory> userInOperands,
DynamicSet<ServerStateProvider> serverStateProviders,
DynamicSet<AccountStateProvider> accountStateProviders,
DynamicSet<AttentionSetListener> attentionSetListeners,
+ DynamicSet<ValidationOptionsListener> validationOptionsListeners,
+ DynamicSet<CommitValidationInfoListener> commitValidationInfoListeners,
DynamicMap<ReviewerSuggestion> reviewerSuggestions) {
this.accountIndexedListeners = accountIndexedListeners;
this.changeIndexedListeners = changeIndexedListeners;
@@ -200,9 +209,12 @@
this.reviewerDeletedListeners = reviewerDeletedListeners;
this.hasOperands = hasOperands;
this.isOperands = isOperands;
+ this.userInOperands = userInOperands;
this.serverStateProviders = serverStateProviders;
this.accountStateProviders = accountStateProviders;
this.attentionSetListeners = attentionSetListeners;
+ this.validationOptionsListeners = validationOptionsListeners;
+ this.commitValidationInfoListeners = commitValidationInfoListeners;
this.reviewerSuggestions = reviewerSuggestions;
}
@@ -280,6 +292,11 @@
}
@CanIgnoreReturnValue
+ public Registration add(UserInOperandFactory userInOperand, String exportName) {
+ return add(userInOperands, userInOperand, exportName);
+ }
+
+ @CanIgnoreReturnValue
public Registration add(ChangeMessageModifier changeMessageModifier) {
return add(changeMessageModifiers, changeMessageModifier);
}
@@ -396,6 +413,16 @@
}
@CanIgnoreReturnValue
+ public Registration add(ValidationOptionsListener validationOptionsListener) {
+ return add(validationOptionsListeners, validationOptionsListener);
+ }
+
+ @CanIgnoreReturnValue
+ public Registration add(CommitValidationInfoListener commitValidationInfoListener) {
+ return add(commitValidationInfoListeners, commitValidationInfoListener);
+ }
+
+ @CanIgnoreReturnValue
public Registration add(CapabilityDefinition capabilityDefinition, String exportName) {
return add(capabilityDefinitions, capabilityDefinition, exportName);
}
diff --git a/java/com/google/gerrit/acceptance/HttpResponse.java b/java/com/google/gerrit/acceptance/HttpResponse.java
index 76c0f04..5f5d3b2 100644
--- a/java/com/google/gerrit/acceptance/HttpResponse.java
+++ b/java/com/google/gerrit/acceptance/HttpResponse.java
@@ -78,9 +78,13 @@
}
public String getEntityContent() throws IOException {
+ var buf = getRawContent();
+ return RawParseUtils.decode(buf.array(), buf.arrayOffset(), buf.limit()).trim();
+ }
+
+ public ByteBuffer getRawContent() throws IOException {
requireNonNull(response, "Response is not initialized.");
requireNonNull(response.getEntity(), "Response.Entity is not initialized.");
- ByteBuffer buf = IO.readWholeStream(response.getEntity().getContent(), 1024);
- return RawParseUtils.decode(buf.array(), buf.arrayOffset(), buf.limit()).trim();
+ return IO.readWholeStream(response.getEntity().getContent(), 1024);
}
}
diff --git a/java/com/google/gerrit/acceptance/InProcessProtocol.java b/java/com/google/gerrit/acceptance/InProcessProtocol.java
index abcc108..8907fae 100644
--- a/java/com/google/gerrit/acceptance/InProcessProtocol.java
+++ b/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -324,7 +324,7 @@
.orElseThrow(
() -> new RuntimeException(String.format("project %s not found", req.project)));
- AsyncReceiveCommits arc = factory.create(projectState, identifiedUser, db, null);
+ AsyncReceiveCommits arc = factory.create(projectState, identifiedUser, db, null, null);
if (arc.canUpload() != Capable.OK) {
throw new ServiceNotAuthorizedException();
}
diff --git a/java/com/google/gerrit/acceptance/AssertUtil.java b/java/com/google/gerrit/acceptance/PreferencesAssertionUtil.java
similarity index 74%
rename from java/com/google/gerrit/acceptance/AssertUtil.java
rename to java/com/google/gerrit/acceptance/PreferencesAssertionUtil.java
index f72c6d3..8893e31 100644
--- a/java/com/google/gerrit/acceptance/AssertUtil.java
+++ b/java/com/google/gerrit/acceptance/PreferencesAssertionUtil.java
@@ -15,6 +15,7 @@
package com.google.gerrit.acceptance;
import static com.google.common.truth.Truth.assertWithMessage;
+import static com.google.gerrit.extensions.client.NullableBooleanPreferencesFieldComparator.equalBooleanPreferencesFields;
import static com.google.gerrit.server.config.ConfigUtil.skipField;
import java.lang.reflect.Field;
@@ -22,7 +23,9 @@
import java.util.HashSet;
import java.util.Set;
-public class AssertUtil {
+/** Utility class for preferences assertion. */
+public class PreferencesAssertionUtil {
+ /** Asserts preferences classes equality, ignoring the specified fields. */
public static <T> void assertPrefs(T actual, T expected, String... fieldsToExclude)
throws IllegalArgumentException, IllegalAccessException {
Set<String> excludedFields = new HashSet<>(Arrays.asList(fieldsToExclude));
@@ -33,12 +36,10 @@
Object actualVal = field.get(actual);
Object expectedVal = field.get(expected);
if (field.getType().isAssignableFrom(Boolean.class)) {
- if (actualVal == null) {
- actualVal = false;
- }
- if (expectedVal == null) {
- expectedVal = false;
- }
+ assertWithMessage("%s [actual: %s, expected: %s]", field.getName(), actualVal, expectedVal)
+ .that(equalBooleanPreferencesFields((Boolean) expectedVal, (Boolean) actualVal))
+ .isTrue();
+ continue;
}
assertWithMessage(field.getName()).that(actualVal).isEqualTo(expectedVal);
}
diff --git a/java/com/google/gerrit/acceptance/TestExtensions.java b/java/com/google/gerrit/acceptance/TestExtensions.java
new file mode 100644
index 0000000..85cd3cb
--- /dev/null
+++ b/java/com/google/gerrit/acceptance/TestExtensions.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.CHANGE_MODIFICATION;
+import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.DIRECT_PUSH;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.server.ValidationOptionsListener;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationInfo;
+import com.google.gerrit.server.git.validators.CommitValidationInfoListener;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.update.context.RefUpdateContext;
+import java.util.List;
+
+/**
+ * Class to host common test extension implementations.
+ *
+ * <p>To test the invocation of an extension point tests usually register a test implementation for
+ * the extension that records the parameters with which it has been called.
+ *
+ * <p>If the same extension point is triggered by different actions, these test extension
+ * implementations may be needed in different test classes. To avoid duplicating them in the test
+ * classes, they can be added to this class and then be reused from the different tests.
+ */
+public class TestExtensions {
+ public static class TestCommitValidationListener implements CommitValidationListener {
+ public CommitReceivedEvent receiveEvent;
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ this.receiveEvent = receiveEvent;
+ return ImmutableList.of();
+ }
+ }
+
+ public static class TestValidationOptionsListener implements ValidationOptionsListener {
+ public ImmutableListMultimap<String, String> validationOptions;
+
+ @Override
+ public void onPatchSetCreation(
+ BranchNameKey projectAndBranch,
+ PatchSet.Id patchSetId,
+ ImmutableListMultimap<String, String> validationOptions) {
+ this.validationOptions = validationOptions;
+ }
+ }
+
+ public static class TestCommitValidationInfoListener implements CommitValidationInfoListener {
+ public ImmutableMap<String, CommitValidationInfo> validationInfoByValidator;
+ public CommitReceivedEvent receiveEvent;
+ @Nullable public PatchSet.Id patchSetId;
+ public boolean hasChangeModificationRefContext;
+ public boolean hasDirectPushRefContext;
+
+ @Override
+ public void commitValidated(
+ ImmutableMap<String, CommitValidationInfo> validationInfoByValidator,
+ CommitReceivedEvent receiveEvent,
+ PatchSet.Id patchSetId) {
+ this.validationInfoByValidator = validationInfoByValidator;
+ this.receiveEvent = receiveEvent;
+ this.patchSetId = patchSetId;
+ this.hasChangeModificationRefContext = RefUpdateContext.hasOpen(CHANGE_MODIFICATION);
+ this.hasDirectPushRefContext = RefUpdateContext.hasOpen(DIRECT_PUSH);
+ }
+ }
+
+ /**
+ * Private constructor to prevent instantiation of this class.
+ *
+ * <p>This class contains only static classes and hence never needs to be instantiated.
+ */
+ private TestExtensions() {}
+}
diff --git a/java/com/google/gerrit/acceptance/TestMetricMaker.java b/java/com/google/gerrit/acceptance/TestMetricMaker.java
index 85233f2..f7e0667 100644
--- a/java/com/google/gerrit/acceptance/TestMetricMaker.java
+++ b/java/com/google/gerrit/acceptance/TestMetricMaker.java
@@ -15,8 +15,11 @@
package com.google.gerrit.acceptance;
import com.google.auto.value.AutoValue;
+import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.UsedAt;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.metrics.Counter0;
import com.google.gerrit.metrics.Counter1;
import com.google.gerrit.metrics.Counter2;
@@ -59,6 +62,7 @@
public class TestMetricMaker extends DisabledMetricMaker {
private final ConcurrentHashMap<CounterKey, MutableLong> counts = new ConcurrentHashMap<>();
private final ConcurrentHashMap<CounterKey, MutableLong> timers = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<String, Supplier<?>> callbackMetrics = new ConcurrentHashMap<>();
public long getCount(String counterName, Object... fieldValues) {
return getCounterValue(CounterKey.create(counterName, fieldValues)).longValue();
@@ -68,9 +72,14 @@
return getTimerValue(CounterKey.create(timerName)).longValue();
}
+ public Object getCallbackMetricValue(String name) {
+ return callbackMetrics.get(name).get();
+ }
+
public void reset() {
counts.clear();
timers.clear();
+ callbackMetrics.clear();
}
private MutableLong getCounterValue(CounterKey counterKey) {
@@ -149,6 +158,17 @@
};
}
+ @Override
+ @CanIgnoreReturnValue
+ public <V> RegistrationHandle newCallbackMetric(
+ String name, Class<V> valueClass, Description desc, Supplier<V> trigger) {
+ callbackMetrics.put(name, trigger);
+ return new RegistrationHandle() {
+ @Override
+ public void remove() {}
+ };
+ }
+
@AutoValue
abstract static class CounterKey {
abstract String name();
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
index 971347c..daf7d4a 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
@@ -152,6 +152,7 @@
inserter.setGroups(getGroups(changeCreation));
changeCreation.topic().ifPresent(t -> inserter.setTopic(t));
inserter.setApprovals(changeCreation.approvals());
+ inserter.setValidationOptions(changeCreation.validationOptions());
try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, changeOwner, now)) {
batchUpdate.setRepository(repository, revWalk, objectInserter);
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
index eb714d45..9f51510 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/TestChangeCreation.java
@@ -18,6 +18,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
@@ -55,6 +56,8 @@
public abstract ImmutableMap<String, Short> approvals();
+ public abstract ImmutableListMultimap<String, String> validationOptions();
+
public abstract String commitMessage();
public abstract ImmutableList<TreeModification> treeModifications();
@@ -72,7 +75,8 @@
.commitMessage("A test change")
// Which value we choose here doesn't matter. All relevant code paths set the desired value.
.mergeStrategy(MergeStrategy.OURS)
- .approvals(ImmutableMap.of());
+ .approvals(ImmutableMap.of())
+ .validationOptions(ImmutableListMultimap.of());
}
@AutoValue.Builder
@@ -157,6 +161,10 @@
*/
public abstract Builder approvals(ImmutableMap<String, Short> approvals);
+ /** The validation options that should be used for creating this change. */
+ public abstract Builder validationOptions(
+ ImmutableListMultimap<String, String> validationOptions);
+
/**
* The commit message. The message may contain a {@code Change-Id} footer but does not need to.
* If the footer is absent, it will be generated.
diff --git a/java/com/google/gerrit/common/data/GlobalCapability.java b/java/com/google/gerrit/common/data/GlobalCapability.java
index c957986..001021f 100644
--- a/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -59,6 +59,9 @@
/** Can create any group on the server. */
public static final String CREATE_GROUP = "createGroup";
+ /** Can delete internal group on the server. */
+ public static final String DELETE_GROUP = "deleteGroup";
+
/** Can create any project on the server. */
public static final String CREATE_PROJECT = "createProject";
@@ -145,6 +148,7 @@
NAMES_ALL.add(BATCH_CHANGES_LIMIT);
NAMES_ALL.add(CREATE_ACCOUNT);
NAMES_ALL.add(CREATE_GROUP);
+ NAMES_ALL.add(DELETE_GROUP);
NAMES_ALL.add(CREATE_PROJECT);
NAMES_ALL.add(EMAIL_REVIEWERS);
NAMES_ALL.add(FLUSH_CACHES);
diff --git a/java/com/google/gerrit/entities/PatchSet.java b/java/com/google/gerrit/entities/PatchSet.java
index 6f71874..b3f685d 100644
--- a/java/com/google/gerrit/entities/PatchSet.java
+++ b/java/com/google/gerrit/entities/PatchSet.java
@@ -195,6 +195,8 @@
public abstract Optional<String> description();
+ public abstract Builder conflicts(Optional<Conflicts> conflicts);
+
public abstract PatchSet build();
}
@@ -268,6 +270,16 @@
*/
public abstract Optional<String> description();
+ /**
+ * Information about conflicts in this patch set.
+ *
+ * <p>Only set for patch sets that are created by Gerrit as a result of performing a Git merge.
+ *
+ * <p>If this field is not set it's unknown whether this patch set contains any file with
+ * conflicts.
+ */
+ public abstract Optional<Conflicts> conflicts();
+
/** Patch set number. */
public int number() {
return id().get();
@@ -277,4 +289,46 @@
public String refName() {
return id().toRefName();
}
+
+ @AutoValue
+ @ConvertibleToProto
+ public abstract static class Conflicts {
+ /**
+ * The SHA1 of the commit that was used as {@code ours} for the Git merge that created the
+ * revision.
+ *
+ * <p>Guaranteed to be set if {@link #containsConflicts()} is {@code true}. If {@link
+ * #containsConflicts()} is {@code false}, only set if the revision was created by Gerrit as a
+ * result of performing a Git merge.
+ */
+ public abstract Optional<ObjectId> ours();
+
+ /**
+ * The SHA1 of the commit that was used as {@code theirs} for the Git merge that created the
+ * revision.
+ *
+ * <p>Guaranteed to be set if {@link #containsConflicts()} is {@code true}. If {@link
+ * #containsConflicts()} is {@code false}, only set if the revision was created by Gerrit as a
+ * result of performing a Git merge.
+ */
+ public abstract Optional<ObjectId> theirs();
+
+ /**
+ * Whether any of the files in the revision has a conflict due to merging {@link #ours} and
+ * {@link #theirs}.
+ *
+ * <p>If {@code true} at least one of the files in the revision has a conflict and contains Git
+ * conflict markers.
+ *
+ * <p>If {@code false} merging {@link #ours} and {@link #theirs} didn't have any conflict. In
+ * this case the files in the revision may only contain Git conflict marker if they were already
+ * present in {@link #ours} or {@link #theirs}.
+ */
+ public abstract boolean containsConflicts();
+
+ public static Conflicts create(
+ Optional<ObjectId> ours, Optional<ObjectId> theirs, boolean containsConflicts) {
+ return new AutoValue_PatchSet_Conflicts(ours, theirs, containsConflicts);
+ }
+ }
}
diff --git a/java/com/google/gerrit/entities/converter/ConflictsProtoConverter.java b/java/com/google/gerrit/entities/converter/ConflictsProtoConverter.java
new file mode 100644
index 0000000..c7dd9e2
--- /dev/null
+++ b/java/com/google/gerrit/entities/converter/ConflictsProtoConverter.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2024 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.entities.converter;
+
+import com.google.errorprone.annotations.Immutable;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.proto.Entities;
+import com.google.protobuf.Parser;
+import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
+
+@Immutable
+public enum ConflictsProtoConverter
+ implements SafeProtoConverter<Entities.Conflicts, PatchSet.Conflicts> {
+ INSTANCE;
+
+ private final ProtoConverter<Entities.ObjectId, ObjectId> objectIdConverter =
+ ObjectIdProtoConverter.INSTANCE;
+
+ @Override
+ public Entities.Conflicts toProto(PatchSet.Conflicts conflicts) {
+ Entities.Conflicts.Builder builder = Entities.Conflicts.newBuilder();
+ conflicts.ours().ifPresent(ours -> builder.setOurs(objectIdConverter.toProto(ours)));
+ conflicts.theirs().ifPresent(theirs -> builder.setTheirs(objectIdConverter.toProto(theirs)));
+ return builder.setContainsConflicts(conflicts.containsConflicts()).build();
+ }
+
+ @Override
+ public PatchSet.Conflicts fromProto(Entities.Conflicts proto) {
+ return PatchSet.Conflicts.create(
+ proto.hasOurs()
+ ? Optional.of(objectIdConverter.fromProto(proto.getOurs()))
+ : Optional.empty(),
+ proto.hasTheirs()
+ ? Optional.of(objectIdConverter.fromProto(proto.getTheirs()))
+ : Optional.empty(),
+ proto.hasContainsConflicts() ? proto.getContainsConflicts() : false);
+ }
+
+ @Override
+ public Parser<Entities.Conflicts> getParser() {
+ return Entities.Conflicts.parser();
+ }
+
+ @Override
+ public Class<Entities.Conflicts> getProtoClass() {
+ return Entities.Conflicts.class;
+ }
+
+ @Override
+ public Class<PatchSet.Conflicts> getEntityClass() {
+ return PatchSet.Conflicts.class;
+ }
+}
diff --git a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
index 22985d9..6412137 100644
--- a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
@@ -21,6 +21,7 @@
import com.google.gerrit.proto.Entities;
import com.google.protobuf.Parser;
import java.time.Instant;
+import java.util.Optional;
import org.eclipse.jgit.lib.ObjectId;
@Immutable
@@ -29,6 +30,8 @@
private final ProtoConverter<Entities.PatchSet_Id, PatchSet.Id> patchSetIdConverter =
PatchSetIdProtoConverter.INSTANCE;
+ private final ProtoConverter<Entities.Conflicts, PatchSet.Conflicts> conflictsConverter =
+ ConflictsProtoConverter.INSTANCE;
private final ProtoConverter<Entities.ObjectId, ObjectId> objectIdConverter =
ObjectIdProtoConverter.INSTANCE;
private final ProtoConverter<Entities.Account_Id, Account.Id> accountIdConverter =
@@ -50,6 +53,9 @@
}
patchSet.pushCertificate().ifPresent(builder::setPushCertificate);
patchSet.description().ifPresent(builder::setDescription);
+ patchSet
+ .conflicts()
+ .ifPresent(conflicts -> builder.setConflicts(conflictsConverter.toProto(conflicts)));
return builder.build();
}
@@ -69,6 +75,9 @@
if (proto.hasBranch()) {
builder.branch(proto.getBranch());
}
+ if (proto.hasConflicts()) {
+ builder.conflicts(Optional.of(conflictsConverter.fromProto(proto.getConflicts())));
+ }
// The following fields used to theoretically be nullable in PatchSet, but in practice no
// production codepath should have ever serialized an instance that was missing one of these
diff --git a/java/com/google/gerrit/extensions/api/GerritApi.java b/java/com/google/gerrit/extensions/api/GerritApi.java
index eebb555..9c2f30e 100644
--- a/java/com/google/gerrit/extensions/api/GerritApi.java
+++ b/java/com/google/gerrit/extensions/api/GerritApi.java
@@ -20,7 +20,6 @@
import com.google.gerrit.extensions.api.groups.Groups;
import com.google.gerrit.extensions.api.plugins.Plugins;
import com.google.gerrit.extensions.api.projects.Projects;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
public interface GerritApi {
Accounts accounts();
@@ -34,40 +33,4 @@
Projects projects();
Plugins plugins();
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements GerritApi {
- @Override
- public Accounts accounts() {
- throw new NotImplementedException();
- }
-
- @Override
- public Changes changes() {
- throw new NotImplementedException();
- }
-
- @Override
- public Config config() {
- throw new NotImplementedException();
- }
-
- @Override
- public Groups groups() {
- throw new NotImplementedException();
- }
-
- @Override
- public Projects projects() {
- throw new NotImplementedException();
- }
-
- @Override
- public Plugins plugins() {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index 251bb5b..a68307a 100644
--- a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -28,7 +28,6 @@
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.SshKeyInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.List;
import java.util.Map;
@@ -138,219 +137,4 @@
String setHttpPassword(String httpPassword) throws RestApiException;
void delete() throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements AccountApi {
- @Override
- public AccountInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public AccountDetailInfo detail() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public AccountStateInfo state() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public boolean getActive() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setActive(boolean active) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public String getAvatarUrl(int size) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public GeneralPreferencesInfo getPreferences() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DiffPreferencesInfo getDiffPreferences() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public EditPreferencesInfo getEditPreferences() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public EditPreferencesInfo setEditPreferences(EditPreferencesInfo in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<ProjectWatchInfo> getWatchedProjects() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<ProjectWatchInfo> setWatchedProjects(List<ProjectWatchInfo> in)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void deleteWatchedProjects(List<ProjectWatchInfo> in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void starChange(String changeId) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void unstarChange(String changeId) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<GroupInfo> getGroups() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<EmailInfo> getEmails() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void addEmail(EmailInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void deleteEmail(String email) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public EmailApi createEmail(EmailInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public EmailApi email(String email) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setStatus(String status) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setDisplayName(String displayName) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<SshKeyInfo> listSshKeys() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SshKeyInfo addSshKey(String key) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void deleteSshKey(int seq) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, GpgKeyInfo> putGpgKeys(List<String> add, List<String> remove)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public GpgKeyApi gpgKey(String id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<AgreementInfo> listAgreements() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void signAgreement(String agreementName) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void index() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<AccountExternalIdInfo> getExternalIds() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void deleteExternalIds(List<String> externalIds) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<DeletedDraftCommentInfo> deleteDraftComments(DeleteDraftCommentsInput input)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setName(String name) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public String generateHttpPassword() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public String setHttpPassword(String httpPassword) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void delete() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/accounts/Accounts.java b/java/com/google/gerrit/extensions/api/accounts/Accounts.java
index ad0d385..34c8eec 100644
--- a/java/com/google/gerrit/extensions/api/accounts/Accounts.java
+++ b/java/com/google/gerrit/extensions/api/accounts/Accounts.java
@@ -17,7 +17,6 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.client.ListAccountsOption;
import com.google.gerrit.extensions.common.AccountInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Arrays;
import java.util.EnumSet;
@@ -218,55 +217,4 @@
return options;
}
}
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements Accounts {
- @Override
- public AccountApi id(String id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public AccountApi id(int id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public AccountApi self() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public AccountApi create(String username) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public AccountApi create(AccountInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SuggestAccountsRequest suggestAccounts() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SuggestAccountsRequest suggestAccounts(String query) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public QueryRequest query() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public QueryRequest query(String query) throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/accounts/EmailApi.java b/java/com/google/gerrit/extensions/api/accounts/EmailApi.java
index da038c3..7a50ea8 100644
--- a/java/com/google/gerrit/extensions/api/accounts/EmailApi.java
+++ b/java/com/google/gerrit/extensions/api/accounts/EmailApi.java
@@ -15,7 +15,6 @@
package com.google.gerrit.extensions.api.accounts;
import com.google.gerrit.extensions.common.EmailInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface EmailApi {
@@ -24,25 +23,4 @@
void delete() throws RestApiException;
void setPreferred() throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements EmailApi {
- @Override
- public EmailInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void delete() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setPreferred() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java b/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
index 6757a05..4ecba2a 100644
--- a/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
+++ b/java/com/google/gerrit/extensions/api/accounts/GpgKeyApi.java
@@ -15,27 +15,10 @@
package com.google.gerrit.extensions.api.accounts;
import com.google.gerrit.extensions.common.GpgKeyInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface GpgKeyApi {
GpgKeyInfo get() throws RestApiException;
void delete() throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements GpgKeyApi {
- @Override
- public GpgKeyInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void delete() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/AttentionSetApi.java b/java/com/google/gerrit/extensions/api/changes/AttentionSetApi.java
index da9a8c7..587f2d2 100644
--- a/java/com/google/gerrit/extensions/api/changes/AttentionSetApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/AttentionSetApi.java
@@ -14,22 +14,10 @@
package com.google.gerrit.extensions.api.changes;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
/** API for managing the attention set of a change. */
public interface AttentionSetApi {
void remove(AttentionSetInput input) throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements AttentionSetApi {
- @Override
- public void remove(AttentionSetInput input) throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index dec3125..5f7ed4d 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -36,7 +36,6 @@
import com.google.gerrit.extensions.common.SubmitRequirementInput;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Arrays;
@@ -599,287 +598,4 @@
return reviewerState;
}
}
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements ChangeApi {
- @Override
- public String id() {
- throw new NotImplementedException();
- }
-
- @Override
- public ReviewerApi reviewer(String id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public RevisionApi revision(String id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void abandon(AbandonInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void restore(RestoreInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void move(MoveInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setPrivate(boolean value, @Nullable String message) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setWorkInProgress(String message) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setReadyForReview(String message) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeApi revert(RevertInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public RevertSubmissionInfo revertSubmission(RevertInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void rebase(RebaseInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Response<RebaseChainInfo> rebaseChain(RebaseInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void delete() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public String topic() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void topic(String topic) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public IncludedInInfo includedIn() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ReviewerResult addReviewer(ReviewerInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SuggestedReviewersRequest suggestReviewers() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SuggestedReviewersRequest suggestReviewers(String query) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<ReviewerInfo> reviewers() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo get(
- EnumSet<ListChangesOption> options, ImmutableListMultimap<String, String> pluginOptions)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfoDifference metaDiff(
- @Nullable String oldMetaRevId,
- @Nullable String newMetaRevId,
- EnumSet<ListChangesOption> options,
- ImmutableListMultimap<String, String> pluginOptions)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public CommitMessageInfo getMessage() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setMessage(CommitMessageInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeEditApi edit() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setHashtags(HashtagsInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Set<String> getHashtags() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setCustomKeyedValues(CustomKeyedValuesInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ImmutableMap<String, String> getCustomKeyedValues() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public AttentionSetApi attention(String id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public AccountInfo addToAttentionSet(AttentionSetInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- @Deprecated
- public Map<String, List<CommentInfo>> comments() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- @Deprecated
- public List<CommentInfo> commentsAsList() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public CommentsRequest commentsRequest() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, List<CommentInfo>> drafts() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<CommentInfo> draftsAsList() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DraftsRequest draftsRequest() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo check() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo check(FixInput fix) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public CheckSubmitRequirementRequest checkSubmitRequirementRequest() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SubmitRequirementResultInfo checkSubmitRequirement(SubmitRequirementInput input)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void index() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<ChangeInfo> submittedTogether() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SubmittedTogetherInfo submittedTogether(EnumSet<SubmittedTogetherOption> options)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SubmittedTogetherInfo submittedTogether(
- EnumSet<ListChangesOption> a, EnumSet<SubmittedTogetherOption> b) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo applyPatch(ApplyPatchPatchSetInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public PureRevertInfo pureRevert() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public PureRevertInfo pureRevert(String claimedOriginal) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<ChangeMessageInfo> messages() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeMessageApi message(String id) throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
index 6d99ded..eaf398d 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeEditApi.java
@@ -17,7 +17,6 @@
import com.google.gerrit.extensions.client.ChangeEditDetailOption;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.restapi.BinaryResult;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.EnumSet;
@@ -214,91 +213,4 @@
*/
void modifyIdentity(String name, String email, ChangeEditIdentityType type)
throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements ChangeEditApi {
- @Override
- public ChangeEditDetailRequest detail() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Optional<EditInfo> get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void create() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void delete() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void rebase() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public EditInfo rebase(RebaseChangeEditInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void publish() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void publish(PublishChangeEditInput publishChangeEditInput) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Optional<BinaryResult> getFile(String filePath) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void renameFile(String oldFilePath, String newFilePath) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void restoreFile(String filePath) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void modifyFile(String filePath, FileContentInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void deleteFile(String filePath) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public String getCommitMessage() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void modifyCommitMessage(String newCommitMessage) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void modifyIdentity(String name, String email, ChangeEditIdentityType type)
- throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/ChangeMessageApi.java b/java/com/google/gerrit/extensions/api/changes/ChangeMessageApi.java
index 66356f1..2f7541c 100644
--- a/java/com/google/gerrit/extensions/api/changes/ChangeMessageApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ChangeMessageApi.java
@@ -15,7 +15,6 @@
package com.google.gerrit.extensions.api.changes;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
/** Interface for change message APIs. */
@@ -30,20 +29,4 @@
* @return the change message with its message updated.
*/
ChangeMessageInfo delete(DeleteChangeMessageInput input) throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements ChangeMessageApi {
- @Override
- public ChangeMessageInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeMessageInfo delete(DeleteChangeMessageInput input) throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/Changes.java b/java/com/google/gerrit/extensions/api/changes/Changes.java
index 605a92e..bd838fd 100644
--- a/java/com/google/gerrit/extensions/api/changes/Changes.java
+++ b/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -21,7 +21,6 @@
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Arrays;
import java.util.EnumSet;
@@ -201,50 +200,4 @@
return sb.toString();
}
}
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements Changes {
- @Override
- public ChangeApi id(int id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeApi id(String triplet) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeApi id(String project, String branch, String id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeApi id(String project, int id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeApi create(ChangeInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo createAsInfo(ChangeInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public QueryRequest query() {
- throw new NotImplementedException();
- }
-
- @Override
- public QueryRequest query(String query) {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/CommentApi.java b/java/com/google/gerrit/extensions/api/changes/CommentApi.java
index 9b5e1da..0135f62 100644
--- a/java/com/google/gerrit/extensions/api/changes/CommentApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/CommentApi.java
@@ -16,7 +16,6 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.common.CommentInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface CommentApi {
@@ -33,20 +32,4 @@
*/
@CanIgnoreReturnValue
CommentInfo delete(DeleteCommentInput input) throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements CommentApi {
- @Override
- public CommentInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public CommentInfo delete(DeleteCommentInput input) throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/DraftApi.java b/java/com/google/gerrit/extensions/api/changes/DraftApi.java
index 50816b7..0767b01 100644
--- a/java/com/google/gerrit/extensions/api/changes/DraftApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/DraftApi.java
@@ -16,7 +16,6 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.common.CommentInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface DraftApi extends CommentApi {
@@ -24,20 +23,4 @@
CommentInfo update(DraftInput in) throws RestApiException;
void delete() throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented extends CommentApi.NotImplemented implements DraftApi {
- @Override
- public CommentInfo update(DraftInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void delete() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/FileApi.java b/java/com/google/gerrit/extensions/api/changes/FileApi.java
index e20ac56..b4a5ec5 100644
--- a/java/com/google/gerrit/extensions/api/changes/FileApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/FileApi.java
@@ -18,7 +18,6 @@
import com.google.gerrit.extensions.common.BlameInfo;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.restapi.BinaryResult;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.List;
import java.util.OptionalInt;
@@ -117,45 +116,4 @@
return forBase;
}
}
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements FileApi {
- @Override
- public BinaryResult content() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DiffInfo diff() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DiffInfo diff(String base) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DiffInfo diff(int parent) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DiffRequest diffRequest() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setReviewed(boolean reviewed) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public BlameRequest blameRequest() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java b/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
index 70e456d..485b04b 100644
--- a/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
@@ -14,7 +14,6 @@
package com.google.gerrit.extensions.api.changes;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Map;
@@ -29,35 +28,4 @@
void remove() throws RestApiException;
void remove(DeleteReviewerInput input) throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements ReviewerApi {
- @Override
- public Map<String, Short> votes() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void deleteVote(String label) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void deleteVote(DeleteVoteInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void remove() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void remove(DeleteReviewerInput input) throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
index 69cf25d..a1a5444 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -33,7 +33,6 @@
import com.google.gerrit.extensions.common.TestSubmitRuleInfo;
import com.google.gerrit.extensions.common.TestSubmitRuleInput;
import com.google.gerrit.extensions.restapi.BinaryResult;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.EnumSet;
import java.util.List;
@@ -207,246 +206,4 @@
return uninterestingParent;
}
}
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements RevisionApi {
- @Override
- public ReviewResult review(ReviewInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo submit(SubmitInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeApi cherryPick(CherryPickInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo cherryPickAsInfo(CherryPickInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeApi rebase(RebaseInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo rebaseAsInfo(RebaseInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public boolean canRebase() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public RevisionReviewerApi reviewer(String id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setReviewed(String path, boolean reviewed) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Set<String> reviewed() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public MergeableInfo mergeable() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public MergeableInfo mergeableOtherBranches() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, FileInfo> files(String base) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, FileInfo> files(int parentNum) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<String> queryFiles(String query) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public FileApi file(String path) {
- throw new NotImplementedException();
- }
-
- @Override
- public CommitInfo commit(boolean addLinks) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, List<CommentInfo>> comments() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<CommentInfo> commentsAsList() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<CommentInfo> draftsAsList() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<RobotCommentInfo> robotCommentsAsList() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, List<CommentInfo>> portedComments() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, List<CommentInfo>> portedDrafts() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public EditInfo applyFix(String fixId) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public EditInfo applyFix(ApplyProvidedFixInput applyProvidedFixInput) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, DiffInfo> getFixPreview(String fixId) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, DiffInfo> getFixPreview(ApplyProvidedFixInput applyProvidedFixInput)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, List<CommentInfo>> drafts() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DraftApi createDraft(DraftInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DraftApi draft(String id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public CommentApi comment(String id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public RobotCommentApi robotComment(String id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public BinaryResult patch() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public BinaryResult patch(String path) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, ActionInfo> actions() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SubmitType submitType() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SubmitType testSubmitType(TestSubmitRuleInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public TestSubmitRuleInfo testSubmitRule(TestSubmitRuleInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public MergeListRequest getMergeList() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public RelatedChangesInfo related() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public RelatedChangesInfo related(EnumSet<GetRelatedOption> options) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ListMultimap<String, ApprovalInfo> votes() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void description(String description) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public String description() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public String etag() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public BinaryResult getArchive(ArchiveFormat format) throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/RevisionReviewerApi.java b/java/com/google/gerrit/extensions/api/changes/RevisionReviewerApi.java
index ec2d5d6..07eb7e0 100644
--- a/java/com/google/gerrit/extensions/api/changes/RevisionReviewerApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RevisionReviewerApi.java
@@ -14,7 +14,6 @@
package com.google.gerrit.extensions.api.changes;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Map;
@@ -24,25 +23,4 @@
void deleteVote(String label) throws RestApiException;
void deleteVote(DeleteVoteInput input) throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements RevisionReviewerApi {
- @Override
- public Map<String, Short> votes() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void deleteVote(String label) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void deleteVote(DeleteVoteInput input) throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/changes/RobotCommentApi.java b/java/com/google/gerrit/extensions/api/changes/RobotCommentApi.java
index e44f21f..8ff4e95 100644
--- a/java/com/google/gerrit/extensions/api/changes/RobotCommentApi.java
+++ b/java/com/google/gerrit/extensions/api/changes/RobotCommentApi.java
@@ -15,20 +15,8 @@
package com.google.gerrit.extensions.api.changes;
import com.google.gerrit.extensions.common.RobotCommentInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface RobotCommentApi {
RobotCommentInfo get() throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements RobotCommentApi {
- @Override
- public RobotCommentInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/config/Config.java b/java/com/google/gerrit/extensions/api/config/Config.java
index 041e1dd..b649da2 100644
--- a/java/com/google/gerrit/extensions/api/config/Config.java
+++ b/java/com/google/gerrit/extensions/api/config/Config.java
@@ -14,20 +14,7 @@
package com.google.gerrit.extensions.api.config;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
-
public interface Config {
/** Returns an API for getting server related configurations. */
Server server();
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements Config {
- @Override
- public Server server() {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/config/ExperimentApi.java b/java/com/google/gerrit/extensions/api/config/ExperimentApi.java
index fb30884..ad17b75 100644
--- a/java/com/google/gerrit/extensions/api/config/ExperimentApi.java
+++ b/java/com/google/gerrit/extensions/api/config/ExperimentApi.java
@@ -15,16 +15,8 @@
package com.google.gerrit.extensions.api.config;
import com.google.gerrit.extensions.common.ExperimentInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface ExperimentApi {
ExperimentInfo get() throws RestApiException;
-
- class NotImplemented implements ExperimentApi {
- @Override
- public ExperimentInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/config/Server.java b/java/com/google/gerrit/extensions/api/config/Server.java
index 26806d1..f3bd3ed 100644
--- a/java/com/google/gerrit/extensions/api/config/Server.java
+++ b/java/com/google/gerrit/extensions/api/config/Server.java
@@ -21,7 +21,6 @@
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.common.ExperimentInfo;
import com.google.gerrit.extensions.common.ServerInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.webui.TopMenu;
import java.util.List;
@@ -69,73 +68,4 @@
return enabledOnly;
}
}
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements Server {
- @Override
- public String getVersion() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ServerInfo getInfo() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public GeneralPreferencesInfo getDefaultPreferences() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public GeneralPreferencesInfo setDefaultPreferences(GeneralPreferencesInfo in)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DiffPreferencesInfo getDefaultDiffPreferences() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public EditPreferencesInfo getDefaultEditPreferences() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public EditPreferencesInfo setDefaultEditPreferences(EditPreferencesInfo in)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<TopMenu.MenuEntry> topMenus() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ExperimentApi experiment(String name) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ListExperimentsRequest listExperiments() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/groups/GroupApi.java b/java/com/google/gerrit/extensions/api/groups/GroupApi.java
index e1b3a9f..671a19c 100644
--- a/java/com/google/gerrit/extensions/api/groups/GroupApi.java
+++ b/java/com/google/gerrit/extensions/api/groups/GroupApi.java
@@ -18,7 +18,6 @@
import com.google.gerrit.extensions.common.GroupAuditEventInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.GroupOptionsInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Arrays;
import java.util.List;
@@ -40,6 +39,9 @@
*/
void name(String name) throws RestApiException;
+ /** Delete group. */
+ void delete() throws RestApiException;
+
/** Returns owning group info. */
GroupInfo owner() throws RestApiException;
@@ -173,105 +175,4 @@
* <p>Only supported for internal groups.
*/
void index() throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements GroupApi {
- @Override
- public GroupInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public GroupInfo detail() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public String name() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void name(String name) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public GroupInfo owner() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void owner(String owner) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public String description() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void description(String description) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public GroupOptionsInfo options() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void options(GroupOptionsInfo options) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<AccountInfo> members() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<AccountInfo> members(boolean recursive) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void addMembers(List<String> members) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void removeMembers(List<String> members) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<GroupInfo> includedGroups() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void addGroups(List<String> groups) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void removeGroups(List<String> groups) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<? extends GroupAuditEventInfo> auditLog() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void index() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/groups/Groups.java b/java/com/google/gerrit/extensions/api/groups/Groups.java
index 8d53af0..0a9a927 100644
--- a/java/com/google/gerrit/extensions/api/groups/Groups.java
+++ b/java/com/google/gerrit/extensions/api/groups/Groups.java
@@ -17,7 +17,6 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.client.ListGroupsOption;
import com.google.gerrit.extensions.common.GroupInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -291,40 +290,4 @@
return options;
}
}
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements Groups {
- @Override
- public GroupApi id(String id) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public GroupApi create(String name) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public GroupApi create(GroupInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ListRequest list() {
- throw new NotImplementedException();
- }
-
- @Override
- public QueryRequest query() {
- throw new NotImplementedException();
- }
-
- @Override
- public QueryRequest query(String query) {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/plugins/PluginApi.java b/java/com/google/gerrit/extensions/api/plugins/PluginApi.java
index b6d78a3..b0d5bdc 100644
--- a/java/com/google/gerrit/extensions/api/plugins/PluginApi.java
+++ b/java/com/google/gerrit/extensions/api/plugins/PluginApi.java
@@ -15,7 +15,6 @@
package com.google.gerrit.extensions.api.plugins;
import com.google.gerrit.extensions.common.PluginInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface PluginApi {
@@ -26,30 +25,4 @@
void disable() throws RestApiException;
void reload() throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements PluginApi {
- @Override
- public PluginInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void enable() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void disable() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void reload() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/plugins/Plugins.java b/java/com/google/gerrit/extensions/api/plugins/Plugins.java
index fed8507..9ab67fa 100644
--- a/java/com/google/gerrit/extensions/api/plugins/Plugins.java
+++ b/java/com/google/gerrit/extensions/api/plugins/Plugins.java
@@ -16,7 +16,6 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.common.PluginInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.ArrayList;
import java.util.List;
@@ -110,33 +109,4 @@
return regex;
}
}
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements Plugins {
- @Override
- public ListRequest list() {
- throw new NotImplementedException();
- }
-
- @Override
- public PluginApi name(String name) {
- throw new NotImplementedException();
- }
-
- @Override
- @Deprecated
- public PluginApi install(
- String name, com.google.gerrit.extensions.common.InstallPluginInput input)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public PluginApi install(String name, InstallPluginInput input) {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/projects/BranchApi.java b/java/com/google/gerrit/extensions/api/projects/BranchApi.java
index a410205..6d30c1b 100644
--- a/java/com/google/gerrit/extensions/api/projects/BranchApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/BranchApi.java
@@ -17,7 +17,6 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.api.changes.ChangeApi.SuggestedReviewersRequest;
import com.google.gerrit.extensions.restapi.BinaryResult;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.List;
@@ -43,45 +42,4 @@
default SuggestedReviewersRequest suggestCcs(String query) throws RestApiException {
return suggestReviewers().forCc().withQuery(query);
}
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements BranchApi {
- @Override
- public BranchApi create(BranchInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SuggestedReviewersRequest suggestReviewers() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SuggestedReviewersRequest suggestReviewers(String query) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public BranchInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void delete() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public BinaryResult file(String path) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<ReflogEntryInfo> reflog() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java b/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
index 146ef27..c471ef7 100644
--- a/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ChildProjectApi.java
@@ -15,27 +15,10 @@
package com.google.gerrit.extensions.api.projects;
import com.google.gerrit.extensions.common.ProjectInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface ChildProjectApi {
ProjectInfo get() throws RestApiException;
ProjectInfo get(boolean recursive) throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements ChildProjectApi {
- @Override
- public ProjectInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ProjectInfo get(boolean recursive) throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/projects/CommitApi.java b/java/com/google/gerrit/extensions/api/projects/CommitApi.java
index b0cc9da..18ba0c0 100644
--- a/java/com/google/gerrit/extensions/api/projects/CommitApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/CommitApi.java
@@ -19,7 +19,6 @@
import com.google.gerrit.extensions.api.changes.IncludedInInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.FileInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Map;
@@ -32,27 +31,4 @@
/** List files in a specific commit against the parent commit. */
Map<String, FileInfo> files(int parentNum) throws RestApiException;
-
- /** A default implementation for source compatibility when adding new methods to the interface. */
- class NotImplemented implements CommitApi {
- @Override
- public CommitInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeApi cherryPick(CherryPickInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public IncludedInInfo includedIn() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, FileInfo> files(int parentNum) throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/projects/DashboardApi.java b/java/com/google/gerrit/extensions/api/projects/DashboardApi.java
index 3cde570..8c32b72 100644
--- a/java/com/google/gerrit/extensions/api/projects/DashboardApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/DashboardApi.java
@@ -14,7 +14,6 @@
package com.google.gerrit.extensions.api.projects;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface DashboardApi {
@@ -24,25 +23,4 @@
DashboardInfo get(boolean inherited) throws RestApiException;
void setDefault() throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements DashboardApi {
- @Override
- public DashboardInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DashboardInfo get(boolean inherited) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void setDefault() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/projects/LabelApi.java b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
index a5e23f2..009ac40 100644
--- a/java/com/google/gerrit/extensions/api/projects/LabelApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/LabelApi.java
@@ -18,7 +18,6 @@
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 {
@@ -35,30 +34,4 @@
}
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 58fd93a8..86d597d 100644
--- a/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -26,7 +26,6 @@
import com.google.gerrit.extensions.common.ListTagSortOption;
import com.google.gerrit.extensions.common.ProjectInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.Collection;
import java.util.List;
@@ -306,232 +305,4 @@
*/
@CanIgnoreReturnValue
ChangeInfo submitRequirementsReview(BatchSubmitRequirementInput input) throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements ProjectApi {
- @Override
- public ProjectApi create() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ProjectApi create(ProjectInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ProjectInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public String description() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ProjectAccessInfo access() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo accessChange(ProjectAccessInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public CheckProjectResultInfo check(CheckProjectInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ConfigInfo config() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ConfigInfo config(ConfigInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo configReview(ConfigInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public Map<String, Set<String>> commitsIn(Collection<String> commits, Collection<String> refs)
- throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void description(DescriptionInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ListRefsRequest<BranchInfo> branches() {
- throw new NotImplementedException();
- }
-
- @Override
- public ListRefsRequest<TagInfo> tags() {
- throw new NotImplementedException();
- }
-
- @Override
- public List<ProjectInfo> children() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<ProjectInfo> children(boolean recursive) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public List<ProjectInfo> children(int limit) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChildProjectApi child(String name) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public BranchApi branch(String ref) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public TagApi tag(String ref) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void deleteBranches(DeleteBranchesInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void deleteTags(DeleteTagsInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public CommitApi commit(String commit) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DashboardApi dashboard(String name) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public DashboardApi defaultDashboard() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ListDashboardsRequest dashboards() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void defaultDashboard(String name) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void removeDefaultDashboard() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public String head() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void head(String head) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public String parent() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void parent(String parent) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void index(boolean indexChildren) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void indexChanges() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ListLabelsRequest labels() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ListSubmitRequirementsRequest submitRequirements() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public LabelApi label(String labelName) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SubmitRequirementApi submitRequirement(String name) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void labels(BatchLabelInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo labelsReview(BatchLabelInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void submitRequirements(BatchSubmitRequirementInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ChangeInfo submitRequirementsReview(BatchSubmitRequirementInput input)
- throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/projects/Projects.java b/java/com/google/gerrit/extensions/api/projects/Projects.java
index 7c8ecca..1f4f141 100644
--- a/java/com/google/gerrit/extensions/api/projects/Projects.java
+++ b/java/com/google/gerrit/extensions/api/projects/Projects.java
@@ -17,7 +17,6 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.client.ProjectState;
import com.google.gerrit.extensions.common.ProjectInfo;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import java.util.ArrayList;
import java.util.Collections;
@@ -261,40 +260,4 @@
return start;
}
}
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements Projects {
- @Override
- public ProjectApi name(String name) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ProjectApi create(ProjectInput in) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ProjectApi create(String name) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public ListRequest list() {
- throw new NotImplementedException();
- }
-
- @Override
- public QueryRequest query() {
- throw new NotImplementedException();
- }
-
- @Override
- public QueryRequest query(String query) {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java b/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java
index 29765c0..3556613 100644
--- a/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/SubmitRequirementApi.java
@@ -17,7 +17,6 @@
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.common.SubmitRequirementInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInput;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface SubmitRequirementApi {
@@ -34,30 +33,4 @@
/** Delete existing submit requirement. */
void delete() throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements SubmitRequirementApi {
- @Override
- public SubmitRequirementApi create(SubmitRequirementInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SubmitRequirementInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public SubmitRequirementInfo update(SubmitRequirementInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void delete() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/api/projects/TagApi.java b/java/com/google/gerrit/extensions/api/projects/TagApi.java
index 69c29df..94cddaf 100644
--- a/java/com/google/gerrit/extensions/api/projects/TagApi.java
+++ b/java/com/google/gerrit/extensions/api/projects/TagApi.java
@@ -15,7 +15,6 @@
package com.google.gerrit.extensions.api.projects;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
-import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
public interface TagApi {
@@ -25,25 +24,4 @@
TagInfo get() throws RestApiException;
void delete() throws RestApiException;
-
- /**
- * A default implementation which allows source compatibility when adding new methods to the
- * interface.
- */
- class NotImplemented implements TagApi {
- @Override
- public TagApi create(TagInput input) throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public TagInfo get() throws RestApiException {
- throw new NotImplementedException();
- }
-
- @Override
- public void delete() throws RestApiException {
- throw new NotImplementedException();
- }
- }
}
diff --git a/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java b/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
index ad494cb..8de9826 100644
--- a/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
@@ -14,6 +14,8 @@
package com.google.gerrit.extensions.client;
+import static com.google.gerrit.extensions.client.NullableBooleanPreferencesFieldComparator.equalBooleanPreferencesFields;
+
import com.google.common.base.MoreObjects;
import com.google.gerrit.common.ConvertibleToProto;
import java.util.Objects;
@@ -76,25 +78,26 @@
&& Objects.equals(this.fontSize, other.fontSize)
&& Objects.equals(this.lineLength, other.lineLength)
&& Objects.equals(this.cursorBlinkRate, other.cursorBlinkRate)
- && Objects.equals(this.expandAllComments, other.expandAllComments)
- && Objects.equals(this.intralineDifference, other.intralineDifference)
- && Objects.equals(this.manualReview, other.manualReview)
- && Objects.equals(this.showLineEndings, other.showLineEndings)
- && Objects.equals(this.showTabs, other.showTabs)
- && Objects.equals(this.showWhitespaceErrors, other.showWhitespaceErrors)
- && Objects.equals(this.syntaxHighlighting, other.syntaxHighlighting)
- && Objects.equals(this.hideTopMenu, other.hideTopMenu)
- && Objects.equals(this.autoHideDiffTableHeader, other.autoHideDiffTableHeader)
- && Objects.equals(this.hideLineNumbers, other.hideLineNumbers)
- && Objects.equals(this.renderEntireFile, other.renderEntireFile)
- && Objects.equals(this.hideEmptyPane, other.hideEmptyPane)
- && Objects.equals(this.matchBrackets, other.matchBrackets)
- && Objects.equals(this.lineWrapping, other.lineWrapping)
+ && equalBooleanPreferencesFields(this.expandAllComments, other.expandAllComments)
+ && equalBooleanPreferencesFields(this.intralineDifference, other.intralineDifference)
+ && equalBooleanPreferencesFields(this.manualReview, other.manualReview)
+ && equalBooleanPreferencesFields(this.showLineEndings, other.showLineEndings)
+ && equalBooleanPreferencesFields(this.showTabs, other.showTabs)
+ && equalBooleanPreferencesFields(this.showWhitespaceErrors, other.showWhitespaceErrors)
+ && equalBooleanPreferencesFields(this.syntaxHighlighting, other.syntaxHighlighting)
+ && equalBooleanPreferencesFields(this.hideTopMenu, other.hideTopMenu)
+ && equalBooleanPreferencesFields(
+ this.autoHideDiffTableHeader, other.autoHideDiffTableHeader)
+ && equalBooleanPreferencesFields(this.hideLineNumbers, other.hideLineNumbers)
+ && equalBooleanPreferencesFields(this.renderEntireFile, other.renderEntireFile)
+ && equalBooleanPreferencesFields(this.hideEmptyPane, other.hideEmptyPane)
+ && equalBooleanPreferencesFields(this.matchBrackets, other.matchBrackets)
+ && equalBooleanPreferencesFields(this.lineWrapping, other.lineWrapping)
&& Objects.equals(this.ignoreWhitespace, other.ignoreWhitespace)
- && Objects.equals(this.retainHeader, other.retainHeader)
- && Objects.equals(this.skipDeleted, other.skipDeleted)
- && Objects.equals(this.skipUnchanged, other.skipUnchanged)
- && Objects.equals(this.skipUncommented, other.skipUncommented);
+ && equalBooleanPreferencesFields(this.retainHeader, other.retainHeader)
+ && equalBooleanPreferencesFields(this.skipDeleted, other.skipDeleted)
+ && equalBooleanPreferencesFields(this.skipUnchanged, other.skipUnchanged)
+ && equalBooleanPreferencesFields(this.skipUncommented, other.skipUncommented);
}
@Override
diff --git a/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java b/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
index 5da211e..6e3d097 100644
--- a/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
@@ -14,6 +14,8 @@
package com.google.gerrit.extensions.client;
+import static com.google.gerrit.extensions.client.NullableBooleanPreferencesFieldComparator.equalBooleanPreferencesFields;
+
import com.google.common.base.MoreObjects;
import com.google.gerrit.common.ConvertibleToProto;
import java.util.Objects;
@@ -46,16 +48,16 @@
&& Objects.equals(this.lineLength, other.lineLength)
&& Objects.equals(this.indentUnit, other.indentUnit)
&& Objects.equals(this.cursorBlinkRate, other.cursorBlinkRate)
- && Objects.equals(this.hideTopMenu, other.hideTopMenu)
- && Objects.equals(this.showTabs, other.showTabs)
- && Objects.equals(this.showWhitespaceErrors, other.showWhitespaceErrors)
- && Objects.equals(this.syntaxHighlighting, other.syntaxHighlighting)
- && Objects.equals(this.hideLineNumbers, other.hideLineNumbers)
- && Objects.equals(this.matchBrackets, other.matchBrackets)
- && Objects.equals(this.lineWrapping, other.lineWrapping)
- && Objects.equals(this.indentWithTabs, other.indentWithTabs)
- && Objects.equals(this.autoCloseBrackets, other.autoCloseBrackets)
- && Objects.equals(this.showBase, other.showBase);
+ && equalBooleanPreferencesFields(this.hideTopMenu, other.hideTopMenu)
+ && equalBooleanPreferencesFields(this.showTabs, other.showTabs)
+ && equalBooleanPreferencesFields(this.showWhitespaceErrors, other.showWhitespaceErrors)
+ && equalBooleanPreferencesFields(this.syntaxHighlighting, other.syntaxHighlighting)
+ && equalBooleanPreferencesFields(this.hideLineNumbers, other.hideLineNumbers)
+ && equalBooleanPreferencesFields(this.matchBrackets, other.matchBrackets)
+ && equalBooleanPreferencesFields(this.lineWrapping, other.lineWrapping)
+ && equalBooleanPreferencesFields(this.indentWithTabs, other.indentWithTabs)
+ && equalBooleanPreferencesFields(this.autoCloseBrackets, other.autoCloseBrackets)
+ && equalBooleanPreferencesFields(this.showBase, other.showBase);
}
@Override
diff --git a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
index 3cc374a..ffdc276 100644
--- a/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
+++ b/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -14,6 +14,8 @@
package com.google.gerrit.extensions.client;
+import static com.google.gerrit.extensions.client.NullableBooleanPreferencesFieldComparator.equalBooleanPreferencesFields;
+
import com.google.common.base.MoreObjects;
import com.google.gerrit.common.ConvertibleToProto;
import java.util.List;
@@ -202,26 +204,32 @@
&& Objects.equals(this.theme, other.theme)
&& Objects.equals(this.dateFormat, other.dateFormat)
&& Objects.equals(this.timeFormat, other.timeFormat)
- && Objects.equals(this.expandInlineDiffs, other.expandInlineDiffs)
- && Objects.equals(this.relativeDateInChangeTable, other.relativeDateInChangeTable)
+ && equalBooleanPreferencesFields(this.expandInlineDiffs, other.expandInlineDiffs)
+ && equalBooleanPreferencesFields(
+ this.relativeDateInChangeTable, other.relativeDateInChangeTable)
&& Objects.equals(this.diffView, other.diffView)
- && Objects.equals(this.sizeBarInChangeTable, other.sizeBarInChangeTable)
- && Objects.equals(this.legacycidInChangeTable, other.legacycidInChangeTable)
- && Objects.equals(this.muteCommonPathPrefixes, other.muteCommonPathPrefixes)
- && Objects.equals(this.signedOffBy, other.signedOffBy)
+ && equalBooleanPreferencesFields(this.sizeBarInChangeTable, other.sizeBarInChangeTable)
+ && equalBooleanPreferencesFields(this.legacycidInChangeTable, other.legacycidInChangeTable)
+ && equalBooleanPreferencesFields(this.muteCommonPathPrefixes, other.muteCommonPathPrefixes)
+ && equalBooleanPreferencesFields(this.signedOffBy, other.signedOffBy)
&& Objects.equals(this.emailStrategy, other.emailStrategy)
&& Objects.equals(this.emailFormat, other.emailFormat)
&& Objects.equals(this.defaultBaseForMerges, other.defaultBaseForMerges)
- && Objects.equals(this.publishCommentsOnPush, other.publishCommentsOnPush)
- && Objects.equals(this.disableKeyboardShortcuts, other.disableKeyboardShortcuts)
- && Objects.equals(this.disableTokenHighlighting, other.disableTokenHighlighting)
- && Objects.equals(this.workInProgressByDefault, other.workInProgressByDefault)
+ && equalBooleanPreferencesFields(this.publishCommentsOnPush, other.publishCommentsOnPush)
+ && equalBooleanPreferencesFields(
+ this.disableKeyboardShortcuts, other.disableKeyboardShortcuts)
+ && equalBooleanPreferencesFields(
+ this.disableTokenHighlighting, other.disableTokenHighlighting)
+ && equalBooleanPreferencesFields(
+ this.workInProgressByDefault, other.workInProgressByDefault)
&& Objects.equals(this.my, other.my)
&& Objects.equals(this.changeTable, other.changeTable)
- && Objects.equals(this.allowBrowserNotifications, other.allowBrowserNotifications)
- && Objects.equals(
+ && equalBooleanPreferencesFields(
+ this.allowBrowserNotifications, other.allowBrowserNotifications)
+ && equalBooleanPreferencesFields(
this.allowSuggestCodeWhileCommenting, other.allowSuggestCodeWhileCommenting)
- && Objects.equals(this.allowAutocompletingComments, other.allowAutocompletingComments)
+ && equalBooleanPreferencesFields(
+ this.allowAutocompletingComments, other.allowAutocompletingComments)
&& Objects.equals(this.diffPageSidebar, other.diffPageSidebar);
}
diff --git a/java/com/google/gerrit/extensions/client/NullableBooleanPreferencesFieldComparator.java b/java/com/google/gerrit/extensions/client/NullableBooleanPreferencesFieldComparator.java
new file mode 100644
index 0000000..ccccceb
--- /dev/null
+++ b/java/com/google/gerrit/extensions/client/NullableBooleanPreferencesFieldComparator.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2024 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.client;
+
+import com.google.gerrit.common.Nullable;
+import java.util.Objects;
+
+/**
+ * Utility class to compare nullable {@link Boolean} preferences fields.
+ *
+ * <p>This class only meant to be used for comparing preferences fields that are potentially loaded
+ * using {@link com.google.gerrit.server.config.ConfigUtil} (such as {@link GeneralPreferencesInfo},
+ * {@link DiffPreferencesInfo} and {@link EditPreferencesInfo}).
+ */
+public class NullableBooleanPreferencesFieldComparator {
+
+ /**
+ * Compare 2 nullable {@link Boolean} preferences fields, regard to {@code null} as {@code false}.
+ *
+ * <p>{@link com.google.gerrit.server.config.ConfigUtil#loadSection} sets the following values for
+ * Boolean fields, relating to {@code null} as {@code false} the same way:
+ *
+ * <table>
+ * <tr><th> user-def </th> <th> default </th> <th> result </th></tr>
+ * <tr><td> true </td> <td> true </td> <td> true </td></tr>
+ * <tr><td> true </td> <td> false </td> <td> true </td></tr>
+ * <tr><td> true </td> <td> null </td> <td> true </td></tr>
+ * <tr><td> false </td> <td> true </td> <td> false </td></tr>
+ * <tr><td> false </td> <td> false </td> <td> null </td></tr>
+ * <tr><td> false </td> <td> null </td> <td> null </td></tr>
+ * <tr><td> null </td> <td> true </td> <td> true </td></tr>
+ * <tr><td> null </td> <td> false </td> <td> null </td></tr>
+ * <tr><td> null </td> <td> null </td> <td> null </td></tr>
+ * </table>
+ *
+ * When reading the values, the readers always check whether the value is {@code true},
+ * practically referring to {@code null} values as {@code false} anyway. Preferences equality
+ * methods should reflect this state.
+ */
+ public static boolean equalBooleanPreferencesFields(@Nullable Boolean a, @Nullable Boolean b) {
+ return Objects.equals(
+ Objects.requireNonNullElse(a, false), Objects.requireNonNullElse(b, false));
+ }
+}
diff --git a/java/com/google/gerrit/extensions/common/ConflictsInfo.java b/java/com/google/gerrit/extensions/common/ConflictsInfo.java
new file mode 100644
index 0000000..ba9f1be
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/ConflictsInfo.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2024 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;
+
+/** Information about conflicts in a revision. */
+public class ConflictsInfo {
+ /**
+ * The SHA1 of the commit that was used as {@code ours} for the Git merge that created the
+ * revision.
+ *
+ * <p>Guaranteed to be set if {@link #containsConflicts} is {@code true}. If {@link
+ * #containsConflicts} is {@code false}, only set if the revision was created by Gerrit as a
+ * result of performing a Git merge.
+ */
+ public String ours;
+
+ /**
+ * The SHA1 of the commit that was used as {@code theirs} for the Git merge that created the
+ * revision.
+ *
+ * <p>Guaranteed to be set if {@link #containsConflicts} is {@code true}. If {@link
+ * #containsConflicts} is {@code false}, only set if the revision was created by Gerrit as a
+ * result of performing a Git merge.
+ */
+ public String theirs;
+
+ /**
+ * Whether any of the files in the revision has a conflict due to merging {@link #ours} and {@link
+ * #theirs}.
+ *
+ * <p>If {@code true} at least one of the files in the revision has a conflict and contains Git
+ * conflict markers. The conflicts occurred while performing a merge between {@link #ours} and
+ * {@link #theirs}.
+ *
+ * <p>If {@code false}, and {@link #ours} and {@link #theirs} are present, merging {@link #ours}
+ * and {@link #theirs} didn't have any conflict. In this case the files in the revision may only
+ * contain Git conflict markers if they were already present in {@link #ours} or {@link #theirs}.
+ *
+ * <p>If {@code false}, and {@link #ours} and {@link #theirs} are not present, the revision was
+ * not created as a result of performing a Git merge and hence doesn't contain conflicts.
+ */
+ public Boolean containsConflicts;
+}
diff --git a/java/com/google/gerrit/extensions/common/DeleteGroupInput.java b/java/com/google/gerrit/extensions/common/DeleteGroupInput.java
new file mode 100644
index 0000000..302a4a8
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/DeleteGroupInput.java
@@ -0,0 +1,16 @@
+// Copyright (C) 2024 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;
+
+public class DeleteGroupInput {}
diff --git a/java/com/google/gerrit/extensions/common/RevisionInfo.java b/java/com/google/gerrit/extensions/common/RevisionInfo.java
index 7b74a06..dc134fb 100644
--- a/java/com/google/gerrit/extensions/common/RevisionInfo.java
+++ b/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -45,6 +45,15 @@
public PushCertificateInfo pushCertificate;
public String description;
+ /**
+ * Information about conflicts in this revision.
+ *
+ * <p>Only set for revisions that were created by Gerrit as a result of performing a Git merge.
+ *
+ * <p>If this field is not set it's unknown whether the revision contains any file with conflicts.
+ */
+ public ConflictsInfo conflicts;
+
public RevisionInfo() {}
public RevisionInfo(String ref) {
diff --git a/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index dc1758e..d052aa8 100644
--- a/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -536,7 +536,7 @@
}
AsyncReceiveCommits arc =
- factory.create(state, userProvider.get().asIdentifiedUser(), db, null);
+ factory.create(state, userProvider.get().asIdentifiedUser(), db, null, null);
ReceivePack rp = arc.getReceivePack();
req.setAttribute(ATT_ARC, arc);
return rp;
diff --git a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
index f2dbbc2..d7ec2ad 100644
--- a/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
+++ b/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -313,8 +313,13 @@
throws ServletException, IOException {
final long startNanos = System.nanoTime();
long auditStartTs = TimeUtil.nowMs();
- res.setHeader("Content-Disposition", "attachment");
res.setHeader("X-Content-Type-Options", "nosniff");
+ // Nobody should be loading HTML from our API server, but if for some reason that happens, stop
+ // it having any capabilities
+ res.setHeader("Content-Security-Policy", "default-src 'none'; sandbox");
+ res.setHeader("Referrer-Policy", "no-referrer");
+ // Nobody should be iframing our API server.
+ res.setHeader("X-Frame-Options", "deny");
int statusCode = SC_OK;
long responseBytes = -1;
Optional<Exception> cause = Optional.empty();
diff --git a/java/com/google/gerrit/index/IndexCollection.java b/java/com/google/gerrit/index/IndexCollection.java
index 66a9fba..8698908 100644
--- a/java/com/google/gerrit/index/IndexCollection.java
+++ b/java/com/google/gerrit/index/IndexCollection.java
@@ -18,6 +18,8 @@
import com.google.common.collect.Lists;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.MetricMaker;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -25,12 +27,21 @@
/** Dynamic pointers to the index versions used for searching and writing. */
public abstract class IndexCollection<K, V, I extends Index<K, V>> implements LifecycleListener {
+ protected enum IndexType {
+ ACCOUNTS,
+ CHANGES,
+ GROUPS,
+ PROJECTS
+ }
+
private final CopyOnWriteArrayList<I> writeIndexes;
private final AtomicReference<I> searchIndex;
+ private final MetricMaker metrics;
- protected IndexCollection() {
+ protected IndexCollection(MetricMaker metrics) {
this.writeIndexes = Lists.newCopyOnWriteArrayList();
this.searchIndex = new AtomicReference<>();
+ this.metrics = metrics;
}
/** Returns the current search index version. */
@@ -93,7 +104,9 @@
}
@Override
- public void start() {}
+ public void start() {
+ registerMetric();
+ }
@Override
public void stop() {
@@ -107,4 +120,22 @@
}
}
}
+
+ protected abstract IndexType getIndexName();
+
+ private void registerMetric() {
+ String indexName = getIndexName().name().toLowerCase();
+ metrics.newCallbackMetric(
+ String.format("indexes/%s", indexName),
+ Integer.class,
+ new Description(String.format("%s Index documents", indexName))
+ .setGauge()
+ .setUnit("documents"),
+ () -> {
+ if (getSearchIndex() == null) {
+ return -1;
+ }
+ return getSearchIndex().numDocs();
+ });
+ }
}
diff --git a/java/com/google/gerrit/index/project/BUILD b/java/com/google/gerrit/index/project/BUILD
index b029513..28392f0 100644
--- a/java/com/google/gerrit/index/project/BUILD
+++ b/java/com/google/gerrit/index/project/BUILD
@@ -8,6 +8,7 @@
"//java/com/google/gerrit/entities",
"//java/com/google/gerrit/index",
"//java/com/google/gerrit/index:query_exception",
+ "//java/com/google/gerrit/metrics",
"//lib:guava",
"//lib/errorprone:annotations",
"//lib/guice",
diff --git a/java/com/google/gerrit/index/project/ProjectIndexCollection.java b/java/com/google/gerrit/index/project/ProjectIndexCollection.java
index 7ff23c5..cfc745e 100644
--- a/java/com/google/gerrit/index/project/ProjectIndexCollection.java
+++ b/java/com/google/gerrit/index/project/ProjectIndexCollection.java
@@ -17,13 +17,22 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.entities.Project;
import com.google.gerrit.index.IndexCollection;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.inject.Inject;
import com.google.inject.Singleton;
/** Collection of active project indices. See {@link IndexCollection} for details on collections. */
@Singleton
public class ProjectIndexCollection
extends IndexCollection<Project.NameKey, ProjectData, ProjectIndex> {
-
+ @Inject
@VisibleForTesting
- public ProjectIndexCollection() {}
+ public ProjectIndexCollection(MetricMaker metrics) {
+ super(metrics);
+ }
+
+ @Override
+ protected IndexType getIndexName() {
+ return IndexType.PROJECTS;
+ }
}
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index 0950a4c..b7b3835 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -26,7 +26,9 @@
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.Project.NameKey;
import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.SchemaFieldDefs;
@@ -48,6 +50,7 @@
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.group.GroupIndex;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangePredicates;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.time.Instant;
@@ -56,6 +59,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.eclipse.jgit.annotations.Nullable;
@@ -248,6 +252,7 @@
extends AbstractFakeIndex<Change.Id, ChangeData, Map<String, Object>> implements ChangeIndex {
private final ChangeData.Factory changeDataFactory;
private final boolean skipMergable;
+ private final IndexConfig indexConfig;
@Inject
@VisibleForTesting
@@ -255,10 +260,12 @@
SitePaths sitePaths,
ChangeData.Factory changeDataFactory,
@Assisted Schema<ChangeData> schema,
- @GerritServerConfig Config cfg) {
+ @GerritServerConfig Config cfg,
+ IndexConfig indexConfig) {
super(schema, sitePaths, "changes");
this.changeDataFactory = changeDataFactory;
this.skipMergable = !MergeabilityComputationBehavior.fromConfig(cfg).includeInIndex();
+ this.indexConfig = indexConfig;
}
@Override
@@ -315,6 +322,16 @@
public void deleteByValue(ChangeData value) {
delete(ChangeIndex.ENTITY_TO_KEY.apply(value));
}
+
+ @Override
+ public void deleteAllForProject(NameKey project) {
+ QueryOptions opts = QueryOptions.create(indexConfig, 0, Integer.MAX_VALUE, Set.of());
+ DataSource<ChangeData> result = getSource(ChangePredicates.project(project), opts);
+ for (FieldBundle f : result.readRaw().toList()) {
+ int changeNum = f.<Integer>getValue(ChangeField.CHANGENUM_SPEC).intValue();
+ delete(Change.id(changeNum));
+ }
+ }
}
/** Fake implementation of {@link AccountIndex} where all filtering happens in-memory. */
diff --git a/java/com/google/gerrit/lucene/ChangeSubIndex.java b/java/com/google/gerrit/lucene/ChangeSubIndex.java
index 024b102..2bc29d9 100644
--- a/java/com/google/gerrit/lucene/ChangeSubIndex.java
+++ b/java/com/google/gerrit/lucene/ChangeSubIndex.java
@@ -22,6 +22,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.index.QueryOptions;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.Schema.Values;
@@ -105,6 +106,11 @@
}
@Override
+ public void deleteAllForProject(Project.NameKey project) {
+ throw new UnsupportedOperationException("don't use ChangeSubIndex directly");
+ }
+
+ @Override
public DataSource<ChangeData> getSource(Predicate<ChangeData> p, QueryOptions opts)
throws QueryParseException {
throw new UnsupportedOperationException("don't use ChangeSubIndex directly");
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index e5f7787..7b88f45 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -259,6 +259,16 @@
}
@Override
+ public void deleteAllForProject(Project.NameKey project) {
+ Term allForProject = new Term(ChangeField.PROJECT_SPEC.getName(), project.get());
+ try {
+ Futures.allAsList(openIndex.delete(allForProject), closedIndex.delete(allForProject)).get();
+ } catch (ExecutionException | InterruptedException e) {
+ throw new StorageException(e);
+ }
+ }
+
+ @Override
public void deleteAll() {
openIndex.deleteAll();
closedIndex.deleteAll();
diff --git a/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
index f30efd4..7e4ce3c 100644
--- a/java/com/google/gerrit/pgm/util/BatchProgramModule.java
+++ b/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -86,6 +86,7 @@
import com.google.gerrit.server.project.SubmitRequirementsEvaluatorImpl;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.approval.ApprovalModule;
+import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeIsVisibleToPredicate;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
@@ -209,6 +210,7 @@
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeHasOperandFactory.class);
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeIsOperandFactory.class);
+ DynamicMap.mapOf(binder(), ApprovalQueryBuilder.UserInOperandFactory.class);
// Submit rules
DynamicSet.setOf(binder(), SubmitRule.class);
diff --git a/java/com/google/gerrit/server/AclInfoController.java b/java/com/google/gerrit/server/AclInfoController.java
index 1563ba3..9f6e15a 100644
--- a/java/com/google/gerrit/server/AclInfoController.java
+++ b/java/com/google/gerrit/server/AclInfoController.java
@@ -14,31 +14,25 @@
package com.google.gerrit.server;
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.server.logging.LoggingContext;
import com.google.gerrit.server.logging.TraceContext;
-import com.google.gerrit.server.permissions.GlobalPermission;
-import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Optional;
/** Class to control when ACL infos should be collected and be returned to the user. */
+// TODO: re-enable this class if it's found safe, otherwise remove it
@Singleton
public class AclInfoController {
- private final PermissionBackend permissionBackend;
-
- @Inject
- AclInfoController(PermissionBackend permissionBackend) {
- this.permissionBackend = permissionBackend;
- }
-
+ /**
+ * Enable ACL logging if the user has the "View Access" capability.
+ *
+ * @param traceContext the trace context on which ACL logging enabled if the user has the "View
+ * Access" capability.
+ * @throws PermissionBackendException thrown if there is a failure while checking permissions
+ */
public void enableAclLoggingIfUserCanViewAccess(TraceContext traceContext)
throws PermissionBackendException {
- if (canViewAclInfos()) {
- traceContext.enableAclLogging();
- }
+ // intentionally disabled
}
/**
@@ -46,25 +40,7 @@
* Optional#empty()} if ACL logging hasn't been turned on
*/
public Optional<String> getAclInfoMessage() {
- // ACL logging is only enabled if the user can view ACL infos. This is checked when ACL logging
- // is turned on in enableAclLoggingIfUserCanViewAccess. Hence we can return ACL infos if ACL
- // logging is on and do not need to check the permission again. We want to avoid re-checking the
- // permission so that we do not need to handle PermissionBackendException.
- if (!LoggingContext.getInstance().isAclLogging()) {
- return Optional.empty();
- }
-
- ImmutableList<String> aclLogRecords = TraceContext.getAclLogRecords();
- if (aclLogRecords.isEmpty()) {
- aclLogRecords = ImmutableList.of("Found no rules that apply, so defaulting to no permission");
- }
-
- StringBuilder msgBuilder = new StringBuilder("ACL info:");
- aclLogRecords.forEach(aclLogRecord -> msgBuilder.append("\n* ").append(aclLogRecord));
- return Optional.of(msgBuilder.toString());
- }
-
- private boolean canViewAclInfos() throws PermissionBackendException {
- return permissionBackend.currentUser().test(GlobalPermission.VIEW_ACCESS);
+ // intentionally disabled
+ return Optional.empty();
}
}
diff --git a/java/com/google/gerrit/server/RequestCounter.java b/java/com/google/gerrit/server/RequestCounter.java
new file mode 100644
index 0000000..444521a
--- /dev/null
+++ b/java/com/google/gerrit/server/RequestCounter.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.gerrit.common.Nullable;
+
+public interface RequestCounter {
+ /**
+ * Count a request.
+ *
+ * @param requestInfo information about the request
+ * @param error The exception which caused the request to fail, or null if the request was
+ * successful.
+ */
+ void countRequest(RequestInfo requestInfo, @Nullable Throwable error);
+}
diff --git a/java/com/google/gerrit/server/RequestInfo.java b/java/com/google/gerrit/server/RequestInfo.java
index 927985d8..6457b38 100644
--- a/java/com/google/gerrit/server/RequestInfo.java
+++ b/java/com/google/gerrit/server/RequestInfo.java
@@ -85,6 +85,13 @@
return requestUri().map(RequestInfo::redactRequestUri);
}
+ /**
+ * The command name of the SSH command.
+ *
+ * <p>Only set if request type is {@link RequestType#SSH}.
+ */
+ public abstract Optional<String> commandName();
+
/** The user that has sent the request. */
public abstract CurrentUser callingUser();
@@ -164,6 +171,18 @@
return builder().requestType(requestType).callingUser(callingUser).traceContext(traceContext);
}
+ public static RequestInfo.Builder builder(
+ RequestType requestType,
+ String commandName,
+ CurrentUser callingUser,
+ TraceContext traceContext) {
+ return builder()
+ .requestType(requestType)
+ .commandName(commandName)
+ .callingUser(callingUser)
+ .traceContext(traceContext);
+ }
+
@UsedAt(UsedAt.Project.GOOGLE)
public static RequestInfo.Builder builder() {
return new AutoValue_RequestInfo.Builder();
@@ -191,6 +210,8 @@
return this;
}
+ public abstract Builder commandName(String commandName);
+
public abstract Builder callingUser(CurrentUser callingUser);
public abstract Builder traceContext(TraceContext traceContext);
diff --git a/java/com/google/gerrit/server/ValidationOptionsListener.java b/java/com/google/gerrit/server/ValidationOptionsListener.java
new file mode 100644
index 0000000..41a408f
--- /dev/null
+++ b/java/com/google/gerrit/server/ValidationOptionsListener.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Hook to get to know about validation options that have been specified by the user.
+ *
+ * <p>For example, this extension point can be used to log validation options for auditing purposes.
+ */
+@ExtensionPoint
+public interface ValidationOptionsListener {
+ void onPatchSetCreation(
+ BranchNameKey projectAndBranch,
+ PatchSet.Id patchSetId,
+ ImmutableListMultimap<String, String> validationOptions);
+}
diff --git a/java/com/google/gerrit/server/account/AccountLoader.java b/java/com/google/gerrit/server/account/AccountLoader.java
index 42687987..17201a0 100644
--- a/java/com/google/gerrit/server/account/AccountLoader.java
+++ b/java/com/google/gerrit/server/account/AccountLoader.java
@@ -17,6 +17,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.common.AccountInfo;
@@ -37,7 +38,7 @@
import java.util.Set;
public class AccountLoader {
- public static final Set<FillOptions> DETAILED_OPTIONS =
+ public static final Set<FillOptions> DETAILED_OPTIONS_WITHOUT_AVATAR =
Collections.unmodifiableSet(
EnumSet.of(
FillOptions.ID,
@@ -47,9 +48,17 @@
FillOptions.DISPLAY_NAME,
FillOptions.STATUS,
FillOptions.STATE,
- FillOptions.AVATARS,
FillOptions.TAGS));
+ /**
+ * NOTE: loading avatars might be a time consuming operation. Callers which don't display the
+ * avatar should use {@link #DETAILED_OPTIONS_WITHOUT_AVATAR}.
+ */
+ public static final Set<FillOptions> DETAILED_OPTIONS =
+ Collections.unmodifiableSet(
+ Sets.union(
+ AccountLoader.DETAILED_OPTIONS_WITHOUT_AVATAR, EnumSet.of(FillOptions.AVATARS)));
+
public interface Factory {
AccountLoader create(boolean detailed);
diff --git a/java/com/google/gerrit/server/account/CapabilityCollection.java b/java/com/google/gerrit/server/account/CapabilityCollection.java
index 7621929..24c148c 100644
--- a/java/com/google/gerrit/server/account/CapabilityCollection.java
+++ b/java/com/google/gerrit/server/account/CapabilityCollection.java
@@ -51,6 +51,7 @@
public final ImmutableList<PermissionRule> readAs;
public final ImmutableList<PermissionRule> queryLimit;
public final ImmutableList<PermissionRule> createGroup;
+ public final ImmutableList<PermissionRule> deleteGroup;
@Inject
CapabilityCollection(
@@ -100,6 +101,7 @@
readAs = getPermission(GlobalCapability.READ_AS);
queryLimit = getPermission(GlobalCapability.QUERY_LIMIT);
createGroup = getPermission(GlobalCapability.CREATE_GROUP);
+ deleteGroup = getPermission(GlobalCapability.DELETE_GROUP);
}
private static List<PermissionRule> mergeAdmin(
diff --git a/java/com/google/gerrit/server/account/GroupControl.java b/java/com/google/gerrit/server/account/GroupControl.java
index fd18d3e..828cfb6 100644
--- a/java/com/google/gerrit/server/account/GroupControl.java
+++ b/java/com/google/gerrit/server/account/GroupControl.java
@@ -230,4 +230,19 @@
}
return canAdministrateServer();
}
+
+ public boolean canDeleteGroup() {
+ return canAdministrateServer() || hasDeleteGroupCapability();
+ }
+
+ private boolean hasDeleteGroupCapability() {
+ try {
+ return perm.test(GlobalPermission.DELETE_GROUP);
+ } catch (PermissionBackendException e) {
+ logger.atFine().log(
+ "Failed to check %s global capability for user %s",
+ GlobalPermission.DELETE_GROUP, user.getLoggableName());
+ return false;
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
index 64b8ec0..c6ffce5 100644
--- a/java/com/google/gerrit/server/account/InternalAccountDirectory.java
+++ b/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -213,7 +213,7 @@
AvatarProvider ap = avatar.get();
if (ap != null) {
info.avatars = new ArrayList<>();
- IdentifiedUser user = userFactory.create(account.id());
+ IdentifiedUser user = userFactory.create(accountState);
// PolyGerrit UI uses the following sizes for avatars:
// - 32px for avatars next to names e.g. on the dashboard. This is also Gerrit's default.
diff --git a/java/com/google/gerrit/server/account/externalids/DuplicateExternalIdKeyException.java b/java/com/google/gerrit/server/account/externalids/DuplicateExternalIdKeyException.java
index aa09278..503c596 100644
--- a/java/com/google/gerrit/server/account/externalids/DuplicateExternalIdKeyException.java
+++ b/java/com/google/gerrit/server/account/externalids/DuplicateExternalIdKeyException.java
@@ -30,6 +30,11 @@
this.duplicateKey = duplicateKey;
}
+ public DuplicateExternalIdKeyException(ExternalId.Key duplicateKey, Throwable why) {
+ super("Duplicate external ID key: " + duplicateKey.get(), why);
+ this.duplicateKey = duplicateKey;
+ }
+
public ExternalId.Key getDuplicateKey() {
return duplicateKey;
}
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIds.java b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
index 0755a6d..892a222 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalIds.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIds.java
@@ -19,6 +19,7 @@
import com.google.gerrit.entities.Account;
import java.io.IOException;
import java.util.Optional;
+import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
public interface ExternalIds {
@@ -28,6 +29,9 @@
/** Returns the specified external ID. */
Optional<ExternalId> get(ExternalId.Key key) throws IOException;
+ /** Returns the specified external IDs. */
+ ImmutableSet<ExternalId> get(Set<ExternalId.Key> keys) throws IOException;
+
/** Returns the external IDs of the specified account. */
ImmutableSet<ExternalId> byAccount(Account.Id accountId) throws IOException;
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalIdsSameAccountChecker.java b/java/com/google/gerrit/server/account/externalids/ExternalIdsSameAccountChecker.java
new file mode 100644
index 0000000..787583c5
--- /dev/null
+++ b/java/com/google/gerrit/server/account/externalids/ExternalIdsSameAccountChecker.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account.externalids;
+
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.Account;
+
+/** Static utilities for checking that all specified external IDs belong to the same account. */
+public final class ExternalIdsSameAccountChecker {
+ private ExternalIdsSameAccountChecker() {}
+
+ /**
+ * Checks that all specified external IDs belong to the same account.
+ *
+ * @return the ID of the account to which all specified external IDs belong.
+ */
+ public static Account.Id checkSameAccount(Iterable<ExternalId> extIds) {
+ return checkSameAccount(extIds, null);
+ }
+
+ /**
+ * Checks that all specified external IDs belong to specified account. If no account is specified
+ * it is checked that all specified external IDs belong to the same account.
+ *
+ * @return the ID of the account to which all specified external IDs belong.
+ */
+ @CanIgnoreReturnValue
+ public static Account.Id checkSameAccount(
+ Iterable<ExternalId> extIds, @Nullable Account.Id accountId) {
+ for (ExternalId extId : extIds) {
+ if (accountId == null) {
+ accountId = extId.accountId();
+ continue;
+ }
+ checkState(
+ accountId.equals(extId.accountId()),
+ "external id %s belongs to account %s, but expected account %s",
+ extId.key().get(),
+ extId.accountId().get(),
+ accountId.get());
+ }
+ return accountId;
+ }
+}
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java
index 6137884..5f708d2 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdNotes.java
@@ -41,6 +41,7 @@
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdCache;
import com.google.gerrit.server.account.externalids.ExternalIdUpsertPreprocessor;
+import com.google.gerrit.server.account.externalids.ExternalIdsSameAccountChecker;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
@@ -691,12 +692,6 @@
cacheUpdates.add(cu -> cu.remove(removedExtIds));
}
- public void replace(
- Account.Id accountId, Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
- throws IOException, DuplicateExternalIdKeyException {
- replace(accountId, toDelete, toAdd, defaultNoteIdResolver);
- }
-
/**
* Replaces external IDs for an account by external ID keys.
*
@@ -708,15 +703,23 @@
* @throws IllegalStateException is thrown if any of the specified external IDs does not belong to
* the specified account.
*/
- public void replace(
+ private void replace(
Account.Id accountId,
Collection<ExternalId.Key> toDelete,
Collection<ExternalId> toAdd,
- Function<ExternalId, ObjectId> noteIdResolver)
+ Function<ExternalId, ObjectId> noteIdResolver,
+ Collection<ExternalId.Key> externalIdsDeletedInTransaction)
throws IOException, DuplicateExternalIdKeyException {
checkLoaded();
- checkSameAccount(toAdd, accountId);
- checkExternalIdKeysDontExist(ExternalId.Key.from(toAdd), toDelete);
+ Account.Id inferredAccountId = ExternalIdsSameAccountChecker.checkSameAccount(toAdd, accountId);
+ if (inferredAccountId != null && !accountId.equals(inferredAccountId)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "ExternalIdNotes#replace called for account %s, but with external IDs correlated to"
+ + " account %s",
+ accountId, inferredAccountId));
+ }
+ checkExternalIdKeysDontExist(ExternalId.Key.from(toAdd), externalIdsDeletedInTransaction);
Set<ExternalId> removedExtIds = new HashSet<>();
Set<ExternalId> updatedExtIds = new HashSet<>();
@@ -797,13 +800,55 @@
*/
public void replace(Collection<ExternalId> toDelete, Collection<ExternalId> toAdd)
throws IOException, DuplicateExternalIdKeyException {
- Account.Id accountId = checkSameAccount(Iterables.concat(toDelete, toAdd));
+ Account.Id accountId =
+ ExternalIdsSameAccountChecker.checkSameAccount(Iterables.concat(toDelete, toAdd));
if (accountId == null) {
// toDelete and toAdd are empty -> nothing to do
return;
}
- replace(accountId, toDelete.stream().map(ExternalId::key).collect(toSet()), toAdd);
+ Set<ExternalId.Key> toDeleteKeys = toDelete.stream().map(ExternalId::key).collect(toSet());
+ replace(accountId, toDelete, toAdd, /* externalIdsDeletedInTransaction= */ toDeleteKeys);
+ }
+
+ /**
+ * Replaces external IDs.
+ *
+ * <p>Deletion of external IDs is done before adding the new external IDs. This means if an
+ * external ID is specified for deletion and an external ID with the same key is specified to be
+ * added, the old external ID with that key is deleted first and then the new external ID is added
+ * (so the external ID for that key is replaced).
+ *
+ * <p>This method also gets a collection of all external IDs that should be deleted in this
+ * transaction - from the target account or other accounts. This collection is taken into account
+ * when calculating duplications.
+ */
+ public void replace(
+ Account.Id accountId,
+ Collection<ExternalId> toDelete,
+ Collection<ExternalId> toAdd,
+ Collection<ExternalId.Key> externalIdsDeletedInTransaction)
+ throws IOException, DuplicateExternalIdKeyException {
+ Account.Id inferredAccountId =
+ ExternalIdsSameAccountChecker.checkSameAccount(Iterables.concat(toDelete, toAdd));
+ if (inferredAccountId != null && !accountId.equals(inferredAccountId)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "ExternalIdNotes#replace called for account %s, but with external IDs correlated to"
+ + " account %s",
+ accountId, inferredAccountId));
+ }
+ if (toDelete.isEmpty() && toAdd.isEmpty()) {
+ // nothing to do
+ return;
+ }
+
+ replace(
+ accountId,
+ toDelete.stream().map(ExternalId::key).collect(toSet()),
+ toAdd,
+ defaultNoteIdResolver,
+ externalIdsDeletedInTransaction);
}
/**
@@ -822,14 +867,20 @@
Collection<ExternalId> toAdd,
Function<ExternalId, ObjectId> noteIdResolver)
throws IOException, DuplicateExternalIdKeyException {
- Account.Id accountId = checkSameAccount(Iterables.concat(toDelete, toAdd));
+ Account.Id accountId =
+ ExternalIdsSameAccountChecker.checkSameAccount(Iterables.concat(toDelete, toAdd));
if (accountId == null) {
// toDelete and toAdd are empty -> nothing to do
return;
}
+ Set<ExternalId.Key> toDeleteKeys = toDelete.stream().map(ExternalId::key).collect(toSet());
replace(
- accountId, toDelete.stream().map(ExternalId::key).collect(toSet()), toAdd, noteIdResolver);
+ accountId,
+ toDeleteKeys,
+ toAdd,
+ noteIdResolver,
+ /* externalIdsDeletedInTransaction= */ toDeleteKeys);
}
@Override
@@ -889,39 +940,6 @@
}
}
- /**
- * Checks that all specified external IDs belong to the same account.
- *
- * @return the ID of the account to which all specified external IDs belong.
- */
- private static Account.Id checkSameAccount(Iterable<ExternalId> extIds) {
- return checkSameAccount(extIds, null);
- }
-
- /**
- * Checks that all specified external IDs belong to specified account. If no account is specified
- * it is checked that all specified external IDs belong to the same account.
- *
- * @return the ID of the account to which all specified external IDs belong.
- */
- @CanIgnoreReturnValue
- public static Account.Id checkSameAccount(
- Iterable<ExternalId> extIds, @Nullable Account.Id accountId) {
- for (ExternalId extId : extIds) {
- if (accountId == null) {
- accountId = extId.accountId();
- continue;
- }
- checkState(
- accountId.equals(extId.accountId()),
- "external id %s belongs to account %s, but expected account %s",
- extId.key().get(),
- extId.accountId().get(),
- accountId.get());
- }
- return accountId;
- }
-
private void incrementalDuplicateDetection(Collection<ExternalId> externalIds) {
externalIds.stream()
.map(ExternalId::key)
diff --git a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsNoteDbImpl.java b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsNoteDbImpl.java
index 4c26442..46d52e1 100644
--- a/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/externalids/storage/notedb/ExternalIdsNoteDbImpl.java
@@ -14,7 +14,9 @@
package com.google.gerrit.server.account.externalids.storage.notedb;
+import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.common.collect.MoreCollectors.toOptional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
@@ -28,6 +30,7 @@
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Optional;
+import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
@@ -71,15 +74,28 @@
@Override
public Optional<ExternalId> get(ExternalId.Key key) throws IOException {
- Optional<ExternalId> externalId = Optional.empty();
- if (authConfig.isUserNameCaseInsensitiveMigrationMode()) {
- externalId =
- externalIdCache.byKey(externalIdKeyFactory.create(key.scheme(), key.id(), false));
+ ImmutableSet<ExternalId> res = get(ImmutableSet.of(key));
+ checkState(res.size() <= 1, "Got multiple matches for external ID [%s]", key);
+ return !res.isEmpty() ? res.stream().collect(toOptional()) : Optional.empty();
+ }
+
+ @Override
+ public ImmutableSet<ExternalId> get(Set<ExternalId.Key> keys) throws IOException {
+ ImmutableSet.Builder<ExternalId> res = ImmutableSet.builder();
+ for (ExternalId.Key key : keys) {
+ Optional<ExternalId> externalId = Optional.empty();
+ if (authConfig.isUserNameCaseInsensitiveMigrationMode()) {
+ externalId =
+ externalIdCache.byKey(externalIdKeyFactory.create(key.scheme(), key.id(), false));
+ }
+ if (externalId.isEmpty()) {
+ externalId = externalIdCache.byKey(key);
+ }
+ if (externalId.isPresent()) {
+ res.add(externalId.get());
+ }
}
- if (!externalId.isPresent()) {
- externalId = externalIdCache.byKey(key);
- }
- return externalId;
+ return res.build();
}
@Override
diff --git a/java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbWriteStorageModule.java b/java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbWriteStorageModule.java
index 6987de5..083f993 100644
--- a/java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbWriteStorageModule.java
+++ b/java/com/google/gerrit/server/account/storage/notedb/AccountNoteDbWriteStorageModule.java
@@ -18,6 +18,11 @@
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.externalids.storage.notedb.ExternalIdNoteDbWriteStorageModule;
+import com.google.gerrit.server.account.storage.notedb.validators.AccountCommitValidator;
+import com.google.gerrit.server.account.storage.notedb.validators.AccountMergeValidator;
+import com.google.gerrit.server.account.storage.notedb.validators.ExternalIdUpdateValidator;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.MergeValidationListener;
import com.google.gerrit.server.index.account.ReindexAccountsAfterRefUpdate;
import com.google.inject.AbstractModule;
@@ -34,5 +39,10 @@
.to(AccountsUpdateNoteDbImpl.FactoryNoReindex.class);
DynamicSet.bind(binder(), GitBatchRefUpdateListener.class)
.to(ReindexAccountsAfterRefUpdate.class);
+
+ // Validators
+ DynamicSet.bind(binder(), CommitValidationListener.class).to(AccountCommitValidator.class);
+ DynamicSet.bind(binder(), CommitValidationListener.class).to(ExternalIdUpdateValidator.class);
+ DynamicSet.bind(binder(), MergeValidationListener.class).to(AccountMergeValidator.class);
}
}
diff --git a/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java b/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
index edc8707..663eddd 100644
--- a/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
+++ b/java/com/google/gerrit/server/account/storage/notedb/AccountsUpdateNoteDbImpl.java
@@ -16,6 +16,9 @@
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableMap.toImmutableMap;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.server.account.externalids.ExternalIdsSameAccountChecker.checkSameAccount;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.ACCOUNTS_UPDATE;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
@@ -25,6 +28,7 @@
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.gerrit.common.Nullable;
@@ -58,9 +62,11 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Function;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -291,22 +297,50 @@
throws IOException, ConfigInvalidException {
return execute(
ImmutableList.of(
- repo -> {
- AccountConfig accountConfig = read(repo, accountId);
- Account account = accountConfig.getNewAccount(committerIdent.getWhenAsInstant());
- AccountState accountState = AccountState.forAccount(account);
- AccountDelta.Builder deltaBuilder = AccountDelta.builder();
- configureDelta(init, accountState, deltaBuilder);
+ new ExecutableUpdate() {
+ @Override
+ public AccountConfig getAccountConfig(Repository repo)
+ throws ConfigInvalidException, IOException {
+ return read(repo, accountId);
+ }
- AccountDelta accountDelta = deltaBuilder.build();
- accountConfig.setAccountDelta(accountDelta);
- updateExternalIdNotes(
- repo, accountConfig.getExternalIdsRev(), accountId, accountDelta);
- CachedPreferences defaultPreferences =
- CachedPreferences.fromLegacyConfig(
- VersionedDefaultPreferences.get(repo, allUsersName));
+ @Override
+ public Optional<AccountDelta> computeAccountDelta(
+ Repository allUsersRepo, AccountConfig accountConfig) throws IOException {
+ Account account =
+ accountConfig.getNewAccount(committerIdent.getWhenAsInstant());
+ AccountState accountState = AccountState.forAccount(account);
+ AccountDelta.Builder deltaBuilder = AccountDelta.builder();
+ configureDelta(init, accountState, deltaBuilder);
- return new UpdatedAccount(message, accountConfig, defaultPreferences, true);
+ return Optional.of(deltaBuilder.build());
+ }
+
+ @Override
+ public UpdatedAccount execute(
+ Repository repo,
+ Set<ExternalId.Key> externalIdsDeletedInTransaction,
+ AccountConfig accountConfig,
+ Optional<AccountDelta> delta)
+ throws IOException, ConfigInvalidException {
+ accountConfig.setAccountDelta(delta.get());
+ updateExternalIdNotes(
+ repo,
+ accountConfig.getExternalIdsRev(),
+ accountId,
+ delta.get(),
+ ImmutableSet.of());
+ CachedPreferences defaultPreferences =
+ CachedPreferences.fromLegacyConfig(
+ VersionedDefaultPreferences.get(repo, allUsersName));
+
+ return new UpdatedAccount(message, accountConfig, defaultPreferences, true);
+ }
+
+ @Override
+ public Account.Id getAccountId() {
+ return accountId;
+ }
}))
.get(0)
.get();
@@ -321,44 +355,79 @@
}
private ExecutableUpdate createExecutableUpdate(UpdateArguments updateArguments) {
- return repo -> {
- AccountConfig accountConfig = read(repo, updateArguments.accountId);
- CachedPreferences defaultPreferences =
- CachedPreferences.fromLegacyConfig(VersionedDefaultPreferences.get(repo, allUsersName));
- Optional<AccountState> accountState =
- AccountsNoteDbImpl.getFromAccountConfig(externalIds, accountConfig, defaultPreferences);
- if (!accountState.isPresent()) {
- return null;
+ return new ExecutableUpdate() {
+ @Override
+ public AccountConfig getAccountConfig(Repository repo)
+ throws ConfigInvalidException, IOException {
+ return read(repo, updateArguments.accountId);
}
- AccountDelta.Builder deltaBuilder = AccountDelta.builder();
- configureDelta(updateArguments.configureDelta, accountState.get(), deltaBuilder);
+ @Override
+ public Optional<AccountDelta> computeAccountDelta(
+ Repository repo, AccountConfig accountConfig) throws IOException, ConfigInvalidException {
+ CachedPreferences defaultPreferences =
+ CachedPreferences.fromLegacyConfig(VersionedDefaultPreferences.get(repo, allUsersName));
+ Optional<AccountState> accountState =
+ AccountsNoteDbImpl.getFromAccountConfig(externalIds, accountConfig, defaultPreferences);
+ if (!accountState.isPresent()) {
+ return Optional.empty();
+ }
- AccountDelta delta = deltaBuilder.build();
- updateExternalIdNotes(
- repo, accountConfig.getExternalIdsRev(), updateArguments.accountId, delta);
+ AccountDelta.Builder deltaBuilder = AccountDelta.builder();
+ configureDelta(updateArguments.configureDelta, accountState.get(), deltaBuilder);
- if (delta.getShouldDeleteAccount().orElse(false)) {
- return new DeletedAccount(updateArguments.message, accountConfig.getRefName());
+ return Optional.of(deltaBuilder.build());
}
- accountConfig.setAccountDelta(delta);
- CachedPreferences cachedDefaultPreferences =
- CachedPreferences.fromLegacyConfig(VersionedDefaultPreferences.get(repo, allUsersName));
- return new UpdatedAccount(
- updateArguments.message, accountConfig, cachedDefaultPreferences, false);
+ @Override
+ @Nullable
+ public UpdatedAccount execute(
+ Repository repo,
+ Set<ExternalId.Key> externalIdsDeletedInTransaction,
+ AccountConfig accountConfig,
+ Optional<AccountDelta> delta)
+ throws IOException, ConfigInvalidException {
+ if (delta.isEmpty()) {
+ return null;
+ }
+ updateExternalIdNotes(
+ repo,
+ accountConfig.getExternalIdsRev(),
+ updateArguments.accountId,
+ delta.get(),
+ externalIdsDeletedInTransaction);
+
+ if (delta.get().getShouldDeleteAccount().orElse(false)) {
+ return new DeletedAccount(updateArguments.message, accountConfig.getRefName());
+ }
+
+ accountConfig.setAccountDelta(delta.get());
+ CachedPreferences cachedDefaultPreferences =
+ CachedPreferences.fromLegacyConfig(VersionedDefaultPreferences.get(repo, allUsersName));
+ return new UpdatedAccount(
+ updateArguments.message, accountConfig, cachedDefaultPreferences, false);
+ }
+
+ @Override
+ public Account.Id getAccountId() {
+ return updateArguments.accountId;
+ }
};
}
private void updateExternalIdNotes(
- Repository allUsersRepo, Optional<ObjectId> rev, Account.Id accountId, AccountDelta update)
+ Repository allUsersRepo,
+ Optional<ObjectId> rev,
+ Account.Id accountId,
+ AccountDelta update,
+ Set<ExternalId.Key> externalIdsDeletedInTransaction)
throws IOException, ConfigInvalidException {
if (update.hasExternalIdUpdates()) {
// Only load the externalIds if they are going to be updated
// This makes e.g. preferences updates faster.
- ExternalIdNotes.checkSameAccount(
+ checkSameAccount(
Iterables.concat(
update.getCreatedExternalIds(),
update.getUpdatedExternalIds(),
@@ -367,7 +436,11 @@
if (externalIdNotes == null) {
externalIdNotes = extIdNotesFactory.load(allUsersRepo, rev.orElse(ObjectId.zeroId()));
}
- externalIdNotes.replace(update.getDeletedExternalIds(), update.getCreatedExternalIds());
+ externalIdNotes.replace(
+ accountId,
+ update.getDeletedExternalIds(),
+ update.getCreatedExternalIds(),
+ externalIdsDeletedInTransaction);
externalIdNotes.upsert(update.getUpdatedExternalIds());
}
}
@@ -399,9 +472,8 @@
accountState.clear();
updatedAccounts.clear();
try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
- for (ExecutableUpdate executableUpdate : executableUpdates) {
- updatedAccounts.add(executableUpdate.execute(allUsersRepo));
- }
+ ExecutableBatchUpdate batch = new ExecutableBatchUpdate(executableUpdates);
+ updatedAccounts.addAll(batch.executeAll(allUsersRepo));
commit(
allUsersRepo,
updatedAccounts.stream().filter(Objects::nonNull).collect(toList()));
@@ -535,9 +607,77 @@
private static void doNothing() {}
- @FunctionalInterface
private interface ExecutableUpdate {
- UpdatedAccount execute(Repository allUsersRepo) throws IOException, ConfigInvalidException;
+ AccountConfig getAccountConfig(Repository allUsersRepo)
+ throws ConfigInvalidException, IOException;
+
+ Optional<AccountDelta> computeAccountDelta(Repository allUsersRepo, AccountConfig accountConfig)
+ throws IOException, ConfigInvalidException;
+
+ @Nullable
+ UpdatedAccount execute(
+ Repository allUsersRepo,
+ Set<ExternalId.Key> externalIdsDeletedInTransaction,
+ AccountConfig accountConfig,
+ Optional<AccountDelta> delta)
+ throws IOException, ConfigInvalidException;
+
+ Account.Id getAccountId();
+ }
+
+ private static class ExecutableBatchUpdate {
+
+ // Note: ImmutableMap is guaranteed to preserve insersion order.
+ private final ImmutableMap<Account.Id, ExecutableUpdate> updatesPerAccount;
+
+ ExecutableBatchUpdate(List<ExecutableUpdate> updates) {
+ updatesPerAccount =
+ updates.stream().collect(toImmutableMap(u -> u.getAccountId(), Function.identity()));
+ }
+
+ List<UpdatedAccount> executeAll(Repository allUsersRepo)
+ throws IOException, ConfigInvalidException {
+ ImmutableMap.Builder<Account.Id, AccountConfig> configPerAccountBuilder =
+ ImmutableMap.builder();
+ for (Map.Entry<Account.Id, ExecutableUpdate> e : updatesPerAccount.entrySet()) {
+ configPerAccountBuilder.put(e.getKey(), e.getValue().getAccountConfig(allUsersRepo));
+ }
+ ImmutableMap<Account.Id, AccountConfig> configPerAccount =
+ configPerAccountBuilder.buildOrThrow();
+
+ ImmutableMap.Builder<Account.Id, Optional<AccountDelta>> deltaPerAccountBuilder =
+ ImmutableMap.builder();
+ for (Map.Entry<Account.Id, ExecutableUpdate> e : updatesPerAccount.entrySet()) {
+ deltaPerAccountBuilder.put(
+ e.getKey(),
+ e.getValue().computeAccountDelta(allUsersRepo, configPerAccount.get(e.getKey())));
+ }
+ ImmutableMap<Account.Id, Optional<AccountDelta>> deltaPerAccount =
+ deltaPerAccountBuilder.buildOrThrow();
+
+ HashSet<ExternalId.Key> externalIdsDeletedInTransaction = new HashSet<>();
+ for (Optional<AccountDelta> delta : deltaPerAccount.values()) {
+ if (delta.isPresent()) {
+ externalIdsDeletedInTransaction.addAll(
+ delta.get().getDeletedExternalIds().stream()
+ .map(ExternalId::key)
+ .collect(toImmutableSet()));
+ }
+ }
+
+ ArrayList<UpdatedAccount> res = new ArrayList<>();
+ for (Map.Entry<Account.Id, ExecutableUpdate> e : updatesPerAccount.entrySet()) {
+ Account.Id accountId = e.getKey();
+ res.add(
+ e.getValue()
+ .execute(
+ allUsersRepo,
+ externalIdsDeletedInTransaction,
+ configPerAccount.get(accountId),
+ deltaPerAccount.get(accountId)));
+ }
+ return res;
+ }
}
private class UpdatedAccount {
diff --git a/java/com/google/gerrit/server/account/storage/notedb/validators/AccountCommitValidator.java b/java/com/google/gerrit/server/account/storage/notedb/validators/AccountCommitValidator.java
new file mode 100644
index 0000000..fe2f83b
--- /dev/null
+++ b/java/com/google/gerrit/server/account/storage/notedb/validators/AccountCommitValidator.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account.storage.notedb.validators;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.validators.AccountValidator;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * Validates that pending account updates in NoteDb are valid according to {@link AccountValidator}.
+ */
+public class AccountCommitValidator implements CommitValidationListener {
+
+ private final GitRepositoryManager repoManager;
+ private final AllUsersName allUsers;
+ private final AccountValidator accountValidator;
+
+ @Inject
+ AccountCommitValidator(
+ GitRepositoryManager repoManager, AllUsersName allUsers, AccountValidator accountValidator) {
+ this.repoManager = repoManager;
+ this.allUsers = allUsers;
+ this.accountValidator = accountValidator;
+ }
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ if (!allUsers.equals(receiveEvent.project.getNameKey())) {
+ return Collections.emptyList();
+ }
+
+ if (receiveEvent.command.getRefName().startsWith(MagicBranch.NEW_CHANGE)) {
+ // no validation on push for review, will be checked on submit by
+ // MergeValidators.AccountMergeValidator
+ return Collections.emptyList();
+ }
+
+ Account.Id accountId = Account.Id.fromRef(receiveEvent.refName);
+ if (accountId == null) {
+ return Collections.emptyList();
+ }
+
+ try (Repository repo = repoManager.openRepository(allUsers)) {
+ List<String> errorMessages =
+ accountValidator.validate(
+ accountId,
+ repo,
+ receiveEvent.revWalk,
+ receiveEvent.command.getOldId(),
+ receiveEvent.commit);
+ if (!errorMessages.isEmpty()) {
+ throw new CommitValidationException(
+ "invalid account configuration",
+ errorMessages.stream()
+ .map(m -> new CommitValidationMessage(m, ValidationMessage.Type.ERROR))
+ .collect(toList()));
+ }
+ } catch (IOException e) {
+ throw new CommitValidationException(
+ String.format("Validating update for account %s failed", accountId.get()), e);
+ }
+ return Collections.emptyList();
+ }
+}
diff --git a/java/com/google/gerrit/server/account/storage/notedb/validators/AccountMergeValidator.java b/java/com/google/gerrit/server/account/storage/notedb/validators/AccountMergeValidator.java
new file mode 100644
index 0000000..f2883e6
--- /dev/null
+++ b/java/com/google/gerrit/server/account/storage/notedb/validators/AccountMergeValidator.java
@@ -0,0 +1,92 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account.storage.notedb.validators;
+
+import com.google.common.base.Joiner;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.BranchNameKey;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountProperties;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.CodeReviewCommit;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.validators.AccountValidator;
+import com.google.gerrit.server.git.validators.MergeValidationException;
+import com.google.gerrit.server.git.validators.MergeValidationListener;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.List;
+import org.eclipse.jgit.lib.Repository;
+
+public class AccountMergeValidator implements MergeValidationListener {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private final AllUsersName allUsersName;
+ private final ChangeData.Factory changeDataFactory;
+ private final AccountValidator accountValidator;
+
+ @Inject
+ public AccountMergeValidator(
+ AllUsersName allUsersName,
+ ChangeData.Factory changeDataFactory,
+ AccountValidator accountValidator) {
+ this.allUsersName = allUsersName;
+ this.changeDataFactory = changeDataFactory;
+ this.accountValidator = accountValidator;
+ }
+
+ @Override
+ public void onPreMerge(
+ Repository repo,
+ CodeReviewRevWalk revWalk,
+ CodeReviewCommit commit,
+ ProjectState destProject,
+ BranchNameKey destBranch,
+ PatchSet.Id patchSetId,
+ IdentifiedUser caller)
+ throws MergeValidationException {
+ Account.Id accountId = Account.Id.fromRef(destBranch.branch());
+ if (!allUsersName.equals(destProject.getNameKey()) || accountId == null) {
+ return;
+ }
+
+ ChangeData cd =
+ changeDataFactory.create(destProject.getProject().getNameKey(), patchSetId.changeId());
+ try {
+ if (!cd.currentFilePaths().contains(AccountProperties.ACCOUNT_CONFIG)) {
+ return;
+ }
+ } catch (StorageException e) {
+ logger.atSevere().withCause(e).log("Cannot validate account update");
+ throw new MergeValidationException("account validation unavailable", e);
+ }
+
+ try {
+ List<String> errorMessages =
+ accountValidator.validate(accountId, repo, revWalk, null, commit);
+ if (!errorMessages.isEmpty()) {
+ throw new MergeValidationException(
+ "invalid account configuration: " + Joiner.on("; ").join(errorMessages));
+ }
+ } catch (IOException e) {
+ logger.atSevere().withCause(e).log("Cannot validate account update");
+ throw new MergeValidationException("account validation unavailable", e);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/server/account/storage/notedb/validators/ExternalIdUpdateValidator.java b/java/com/google/gerrit/server/account/storage/notedb/validators/ExternalIdUpdateValidator.java
new file mode 100644
index 0000000..f4d14b6
--- /dev/null
+++ b/java/com/google/gerrit/server/account/storage/notedb/validators/ExternalIdUpdateValidator.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account.storage.notedb.validators;
+
+import static java.util.stream.Collectors.toList;
+
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/** Validates updates to refs/meta/external-ids. */
+public class ExternalIdUpdateValidator implements CommitValidationListener {
+ private final AllUsersName allUsers;
+ private final AccountCache accountCache;
+ private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
+
+ @Inject
+ ExternalIdUpdateValidator(
+ AllUsersName allUsers,
+ ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
+ AccountCache accountCache) {
+ this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
+ this.allUsers = allUsers;
+ this.accountCache = accountCache;
+ }
+
+ @Override
+ public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ if (allUsers.equals(receiveEvent.project.getNameKey())
+ && RefNames.REFS_EXTERNAL_IDS.equals(receiveEvent.refName)) {
+ try {
+ List<ConsistencyProblemInfo> problems =
+ externalIdsConsistencyChecker.check(accountCache, receiveEvent.commit);
+ List<CommitValidationMessage> msgs =
+ problems.stream()
+ .map(
+ p ->
+ new CommitValidationMessage(
+ p.message,
+ p.status == ConsistencyProblemInfo.Status.ERROR
+ ? ValidationMessage.Type.ERROR
+ : ValidationMessage.Type.OTHER))
+ .collect(toList());
+ if (msgs.stream().anyMatch(ValidationMessage::isError)) {
+ throw new CommitValidationException("invalid external IDs", msgs);
+ }
+ return msgs;
+ } catch (IOException | ConfigInvalidException e) {
+ throw new CommitValidationException("error validating external IDs", e);
+ }
+ }
+ return Collections.emptyList();
+ }
+}
diff --git a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 38510e3..c84bde2 100644
--- a/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -57,6 +57,7 @@
import com.google.gerrit.extensions.common.TestSubmitRuleInput;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
@@ -108,7 +109,7 @@
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
-class RevisionApiImpl extends RevisionApi.NotImplemented {
+class RevisionApiImpl implements RevisionApi {
interface Factory {
RevisionApiImpl create(RevisionResource r);
}
@@ -719,4 +720,9 @@
throw asRestApiException("Cannot get archive", e);
}
}
+
+ @Override
+ public String etag() throws RestApiException {
+ throw new NotImplementedException();
+ }
}
diff --git a/java/com/google/gerrit/server/api/groups/GroupApiImpl.java b/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
index db906a9..c7a2c69 100644
--- a/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
+++ b/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -19,6 +19,7 @@
import com.google.gerrit.extensions.api.groups.GroupApi;
import com.google.gerrit.extensions.api.groups.OwnerInput;
import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.DeleteGroupInput;
import com.google.gerrit.extensions.common.DescriptionInput;
import com.google.gerrit.extensions.common.GroupAuditEventInfo;
import com.google.gerrit.extensions.common.GroupInfo;
@@ -29,6 +30,7 @@
import com.google.gerrit.server.group.GroupResource;
import com.google.gerrit.server.restapi.group.AddMembers;
import com.google.gerrit.server.restapi.group.AddSubgroups;
+import com.google.gerrit.server.restapi.group.DeleteGroup;
import com.google.gerrit.server.restapi.group.DeleteMembers;
import com.google.gerrit.server.restapi.group.DeleteSubgroups;
import com.google.gerrit.server.restapi.group.GetAuditLog;
@@ -58,6 +60,7 @@
private final GetDetail getDetail;
private final GetName getName;
private final PutName putName;
+ private final DeleteGroup deleteGroup;
private final GetOwner getOwner;
private final PutOwner putOwner;
private final GetDescription getDescription;
@@ -80,6 +83,7 @@
GetDetail getDetail,
GetName getName,
PutName putName,
+ DeleteGroup deleteGroup,
GetOwner getOwner,
PutOwner putOwner,
GetDescription getDescription,
@@ -99,6 +103,7 @@
this.getDetail = getDetail;
this.getName = getName;
this.putName = putName;
+ this.deleteGroup = deleteGroup;
this.getOwner = getOwner;
this.putOwner = putOwner;
this.getDescription = getDescription;
@@ -156,6 +161,17 @@
}
@Override
+ public void delete() throws RestApiException {
+ DeleteGroupInput in = new DeleteGroupInput();
+ try {
+ @SuppressWarnings("unused")
+ var unused = deleteGroup.apply(rsrc, in);
+ } catch (Exception e) {
+ throw asRestApiException("Cannot delete group", e);
+ }
+ }
+
+ @Override
public GroupInfo owner() throws RestApiException {
try {
return getOwner.apply(rsrc).value();
diff --git a/java/com/google/gerrit/server/approval/ApprovalCopier.java b/java/com/google/gerrit/server/approval/ApprovalCopier.java
index 9bb3bc9..ba1733e 100644
--- a/java/com/google/gerrit/server/approval/ApprovalCopier.java
+++ b/java/com/google/gerrit/server/approval/ApprovalCopier.java
@@ -19,11 +19,13 @@
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Table;
import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelTypes;
@@ -37,6 +39,9 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.ChangeKindCache;
import com.google.gerrit.server.change.LabelNormalizer;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.experiments.ExperimentFeatures;
+import com.google.gerrit.server.experiments.ExperimentFeaturesConstants;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
@@ -46,6 +51,7 @@
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.approval.ApprovalContext;
import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.RepoView;
import com.google.gerrit.server.util.LabelVote;
import com.google.gerrit.server.util.ManualRequestContext;
@@ -53,8 +59,12 @@
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
@@ -123,56 +133,111 @@
/** The approval. */
public abstract PatchSetApproval patchSetApproval();
- /**
- * Lists the leaf predicates of the copy condition that are fulfilled.
- *
- * <p>Example: The expression
- *
- * <pre>
- * changekind:TRIVIAL_REBASE OR is:MIN
- * </pre>
- *
- * has two leaf predicates:
- *
- * <ul>
- * <li>changekind:TRIVIAL_REBASE
- * <li>is:MIN
- * </ul>
- *
- * This method will return the leaf predicates that are fulfilled, for example if only the
- * first predicate is fulfilled, the returned list will be equal to
- * ["changekind:TRIVIAL_REBASE"].
- *
- * <p>Empty if the label type is missing, if there is no copy condition or if the copy
- * condition is not parseable.
- */
- public abstract ImmutableSet<String> passingAtoms();
-
- /**
- * Lists the leaf predicates of the copy condition that are not fulfilled. See {@link
- * #passingAtoms()} for more details.
- *
- * <p>Empty if the label type is missing, if there is no copy condition or if the copy
- * condition is not parseable.
- */
- public abstract ImmutableSet<String> failingAtoms();
+ /** Details about the evaluation of approval copy condition. */
+ public abstract ApprovalCopyResult approvalCopyResult();
@VisibleForTesting
public static PatchSetApprovalData create(
- PatchSetApproval approval,
- ImmutableSet<String> passingAtoms,
- ImmutableSet<String> failingAtoms) {
- return new AutoValue_ApprovalCopier_Result_PatchSetApprovalData(
- approval, passingAtoms, failingAtoms);
+ PatchSetApproval approval, ApprovalCopyResult copyResult) {
+ return new AutoValue_ApprovalCopier_Result_PatchSetApprovalData(approval, copyResult);
}
private static PatchSetApprovalData createForMissingLabelType(PatchSetApproval approval) {
return new AutoValue_ApprovalCopier_Result_PatchSetApprovalData(
- approval, ImmutableSet.of(), ImmutableSet.of());
+ approval, ApprovalCopyResult.createEvaluationSkipped());
}
}
}
+ /** Result for checking if an approval can be copied to the next patch set. */
+ @AutoValue
+ public abstract static class ApprovalCopyResult {
+ /** Whether the approval can be copied to the next patch set. */
+ public abstract boolean canCopy();
+
+ /** Label's copyCondition */
+ public abstract @Nullable String labelCopyCondition();
+
+ /** Whether the approval can be copied to the next patch set based on label's copyCondition. */
+ public abstract boolean labelCopy();
+
+ /** Condition that forces copy based on server configuration */
+ public abstract @Nullable String copyEnforcement();
+
+ /**
+ * Whether the approval must be copied to the next patch set based on servers copyEnforcement.
+ */
+ public abstract boolean forcedCopy();
+
+ /** Condition that forces copy not to be made based on server configuration */
+ public abstract @Nullable String copyRestriction();
+
+ /**
+ * Whether the approval must be not be copied to the next patch set based on servers
+ * copyRestriction.
+ */
+ public abstract boolean forcedNonCopy();
+
+ /**
+ * Lists the leaf predicates of the copy condition that are fulfilled.
+ *
+ * <p>Example: The expression
+ *
+ * <pre>
+ * changekind:TRIVIAL_REBASE OR is:MIN
+ * </pre>
+ *
+ * has two leaf predicates:
+ *
+ * <ul>
+ * <li>changekind:TRIVIAL_REBASE
+ * <li>is:MIN
+ * </ul>
+ *
+ * This method will return the leaf predicates that are fulfilled, for example if only the first
+ * predicate is fulfilled, the returned list will be equal to ["changekind:TRIVIAL_REBASE"].
+ *
+ * <p>Empty if the label type is missing, if there is no copy condition or if the copy condition
+ * is not parseable.
+ */
+ public abstract ImmutableSet<String> passingAtoms();
+
+ /**
+ * Lists the leaf predicates of the copy condition that are not fulfilled. See {@link
+ * #passingAtoms()} for more details.
+ *
+ * <p>Empty if the label type is missing, if there is no copy condition or if the copy condition
+ * is not parseable.
+ */
+ public abstract ImmutableSet<String> failingAtoms();
+
+ public static ApprovalCopyResult create(
+ @Nullable String labelCopyCondition,
+ boolean labelCopy,
+ @Nullable String copyEnforcement,
+ boolean forcedCopy,
+ @Nullable String copyRestriction,
+ boolean forcedNonCopy,
+ Set<String> passingAtoms,
+ Set<String> failingAtoms) {
+ return new AutoValue_ApprovalCopier_ApprovalCopyResult(
+ forcedCopy || (labelCopy && !forcedNonCopy),
+ labelCopyCondition,
+ labelCopy,
+ copyEnforcement,
+ forcedCopy,
+ copyRestriction,
+ forcedNonCopy,
+ ImmutableSet.copyOf(passingAtoms),
+ ImmutableSet.copyOf(failingAtoms));
+ }
+
+ public static ApprovalCopyResult createEvaluationSkipped() {
+ return new AutoValue_ApprovalCopier_ApprovalCopyResult(
+ false, null, false, null, false, null, false, ImmutableSet.of(), ImmutableSet.of());
+ }
+ }
+
private final GitRepositoryManager repoManager;
private final ProjectCache projectCache;
private final ChangeKindCache changeKindCache;
@@ -180,6 +245,9 @@
private final LabelNormalizer labelNormalizer;
private final ApprovalQueryBuilder approvalQueryBuilder;
private final OneOffRequestContext requestContext;
+ private final ChangeData.Factory changeDataFactory;
+ private final Config cfg;
+ private final ExperimentFeatures experimentFeatures;
@Inject
ApprovalCopier(
@@ -189,7 +257,10 @@
PatchSetUtil psUtil,
LabelNormalizer labelNormalizer,
ApprovalQueryBuilder approvalQueryBuilder,
- OneOffRequestContext requestContext) {
+ OneOffRequestContext requestContext,
+ ChangeData.Factory changeDataFactory,
+ @GerritServerConfig Config cfg,
+ ExperimentFeatures experimentFeatures) {
this.repoManager = repoManager;
this.projectCache = projectCache;
this.changeKindCache = changeKindCache;
@@ -197,6 +268,9 @@
this.labelNormalizer = labelNormalizer;
this.approvalQueryBuilder = approvalQueryBuilder;
this.requestContext = requestContext;
+ this.changeDataFactory = changeDataFactory;
+ this.cfg = cfg;
+ this.experimentFeatures = experimentFeatures;
}
/**
@@ -280,6 +354,7 @@
// Iterate over the follow-up patch sets in order to copy the approval from their prior patch
// set if possible (copy from PS N-1 to PS N).
+ AttributesNodeProvider attributesNodeProvider = repo.createAttributesNodeProvider();
for (PatchSet.Id followUpPatchSetId : followUpPatchSets) {
PatchSet followUpPatchSet = psUtil.get(changeNotes, followUpPatchSetId);
ChangeKind changeKind =
@@ -287,6 +362,7 @@
changeNotes.getProjectName(),
revWalk,
repo.getConfig(),
+ attributesNodeProvider,
priorPatchSet.commitId(),
followUpPatchSet.commitId());
boolean isMerge = isMerge(changeNotes.getProjectName(), revWalk, followUpPatchSet);
@@ -332,12 +408,23 @@
ChangeKind changeKind,
boolean isMerge,
RepoView repoView) {
- if (!labelType.getCopyCondition().isPresent()) {
- return ApprovalCopyResult.createForMissingCopyCondition();
+ String forcedCopyCondition = null;
+ String forcedNonCopyCondition = null;
+ if (experimentFeatures.isFeatureEnabled(
+ ExperimentFeaturesConstants.ENABLE_CENTRAL_OVERRIDE_FOR_CODE_REVIEW_COPY_CONDITION,
+ changeNotes.getProjectName())) {
+ forcedCopyCondition = cfg.getString("label", labelType.getName(), "labelCopyEnforcement");
+ forcedNonCopyCondition = cfg.getString("label", labelType.getName(), "labelCopyRestriction");
+ }
+ String labelCopyCondition = labelType.getCopyCondition().orElse(null);
+ if (Strings.isNullOrEmpty(forcedCopyCondition)
+ && Strings.isNullOrEmpty(forcedNonCopyCondition)
+ && Strings.isNullOrEmpty(labelCopyCondition)) {
+ return ApprovalCopyResult.createEvaluationSkipped();
}
ApprovalContext ctx =
ApprovalContext.create(
- changeNotes,
+ changeDataFactory.create(changeNotes),
sourcePatchSetId,
approverId,
labelType,
@@ -346,39 +433,69 @@
changeKind,
isMerge,
repoView);
+ // Use a request context to run checks as an internal user with expanded visibility. This is
+ // so that the output of the copy condition does not depend on who is running the current
+ // request (e.g. a group used in this query might not be visible to the person sending this
+ // request).
+ try (ManualRequestContext ignored = requestContext.open()) {
+ LinkedHashSet<String> passingAtoms = new LinkedHashSet<>();
+ LinkedHashSet<String> failingAtoms = new LinkedHashSet<>();
+ boolean labelCopy = evaluateCondition(labelCopyCondition, ctx, passingAtoms, failingAtoms);
+ boolean forcedCopy = evaluateCondition(forcedCopyCondition, ctx, passingAtoms, failingAtoms);
+ boolean forcedNonCopy =
+ evaluateCondition(forcedNonCopyCondition, ctx, passingAtoms, failingAtoms);
+ ApprovalCopyResult result =
+ ApprovalCopyResult.create(
+ labelCopyCondition,
+ labelCopy,
+ forcedCopyCondition,
+ forcedCopy,
+ forcedNonCopyCondition,
+ forcedNonCopy,
+ passingAtoms,
+ failingAtoms);
+ logger.atFine().log(
+ "%s copy %s of account %d on change %d from patch set %d to patch set %d"
+ + " (%s%s%spassingAtoms = %s, failingAtoms = %s, changeKind = %s)",
+ result.canCopy() ? "Can" : "Cannot",
+ LabelVote.create(labelType.getName(), approvalValue).format(),
+ approverId.get(),
+ changeNotes.getChangeId().get(),
+ sourcePatchSetId.get(),
+ targetPatchSet.id().get(),
+ !Strings.isNullOrEmpty(labelCopyCondition)
+ ? String.format("copyCondition = %s, ", labelCopyCondition)
+ : "",
+ !Strings.isNullOrEmpty(forcedCopyCondition)
+ ? String.format("copyEnforcement = %s, ", forcedCopyCondition)
+ : "",
+ !Strings.isNullOrEmpty(forcedNonCopyCondition)
+ ? String.format("copyRestriction = %s, ", forcedNonCopyCondition)
+ : "",
+ passingAtoms,
+ failingAtoms,
+ changeKind.name());
+ return result;
+ }
+ }
+
+ private boolean evaluateCondition(
+ @Nullable String copyCondition,
+ ApprovalContext ctx,
+ LinkedHashSet<String> passingAtoms,
+ LinkedHashSet<String> failingAtoms) {
+ if (Strings.isNullOrEmpty(copyCondition)) {
+ return false;
+ }
try {
- // Use a request context to run checks as an internal user with expanded visibility. This is
- // so that the output of the copy condition does not depend on who is running the current
- // request (e.g. a group used in this query might not be visible to the person sending this
- // request).
- try (ManualRequestContext ignored = requestContext.open()) {
- Predicate<ApprovalContext> copyConditionPredicate =
- approvalQueryBuilder.parse(labelType.getCopyCondition().get());
- boolean canCopy = copyConditionPredicate.asMatchable().match(ctx);
- ImmutableSet.Builder<String> passingAtomsBuilder = ImmutableSet.builder();
- ImmutableSet.Builder<String> failingAtomsBuilder = ImmutableSet.builder();
- evaluateAtoms(copyConditionPredicate, ctx, passingAtomsBuilder, failingAtomsBuilder);
- ImmutableSet<String> passingAtoms = passingAtomsBuilder.build();
- ImmutableSet<String> failingAtoms = failingAtomsBuilder.build();
- logger.atFine().log(
- "%s copy %s of account %d on change %d from patch set %d to patch set %d"
- + " (copyCondition = %s, passingAtoms = %s, failingAtoms = %s, changeKind = %s)",
- canCopy ? "Can" : "Cannot",
- LabelVote.create(labelType.getName(), approvalValue).format(),
- approverId.get(),
- changeNotes.getChangeId().get(),
- sourcePatchSetId.get(),
- targetPatchSet.id().get(),
- labelType.getCopyCondition().get(),
- passingAtoms,
- failingAtoms,
- changeKind.name());
- return ApprovalCopyResult.create(canCopy, passingAtoms, failingAtoms);
- }
+ Predicate<ApprovalContext> copyConditionPredicate = approvalQueryBuilder.parse(copyCondition);
+ boolean result = copyConditionPredicate.asMatchable().match(ctx);
+ evaluateAtoms(copyConditionPredicate, ctx, passingAtoms, failingAtoms);
+ return result;
} catch (QueryParseException e) {
logger.atWarning().withCause(e).log(
"Unable to copy label because config is invalid. This should have been caught before.");
- return ApprovalCopyResult.createForNonParseableCopyCondition();
+ return false;
}
}
@@ -419,6 +536,7 @@
projectName,
repoView.getRevWalk(),
repoView.getConfig(),
+ repoView.getAttributesNodeProvider(),
priorPatchSet.getValue().commitId(),
targetPatchSet.commitId());
boolean isMerge = isMerge(projectName, repoView.getRevWalk(), targetPatchSet);
@@ -481,14 +599,11 @@
priorPsa.label(),
priorPsa.accountId(),
Result.PatchSetApprovalData.create(
- copiedApprovalNormalized.get(),
- approvalCopyResult.passingAtoms(),
- approvalCopyResult.failingAtoms()));
+ copiedApprovalNormalized.get(), approvalCopyResult));
}
} else {
outdatedApprovalsBuilder.add(
- Result.PatchSetApprovalData.create(
- priorPsa, approvalCopyResult.passingAtoms(), approvalCopyResult.failingAtoms()));
+ Result.PatchSetApprovalData.create(priorPsa, approvalCopyResult));
continue;
}
}
@@ -521,11 +636,15 @@
private static void evaluateAtoms(
Predicate<ApprovalContext> predicate,
ApprovalContext approvalContext,
- ImmutableSet.Builder<String> passingAtoms,
- ImmutableSet.Builder<String> failingAtoms) {
+ LinkedHashSet<String> passingAtoms,
+ LinkedHashSet<String> failingAtoms) {
if (predicate.isLeaf()) {
+ String predicateString = predicate.getPredicateString();
+ if (passingAtoms.contains(predicateString) || failingAtoms.contains(predicateString)) {
+ return;
+ }
boolean isPassing = predicate.asMatchable().match(approvalContext);
- (isPassing ? passingAtoms : failingAtoms).add(predicate.getPredicateString());
+ (isPassing ? passingAtoms : failingAtoms).add(predicateString);
return;
}
predicate
@@ -534,46 +653,4 @@
childPredicate ->
evaluateAtoms(childPredicate, approvalContext, passingAtoms, failingAtoms));
}
-
- /** Result for checking if an approval can be copied to the next patch set. */
- @AutoValue
- abstract static class ApprovalCopyResult {
- /** Whether the approval can be copied to the next patch set. */
- abstract boolean canCopy();
-
- /**
- * Lists the leaf predicates of the copy condition that are fulfilled. See {@link
- * Result.PatchSetApprovalData#passingAtoms()} for more details.
- *
- * <p>Empty if there is no copy condition or if the copy condition is not parseable.
- */
- abstract ImmutableSet<String> passingAtoms();
-
- /**
- * Lists the leaf predicates of the copy condition that are not fulfilled. See {@link
- * Result.PatchSetApprovalData#passingAtoms()} for more details.
- *
- * <p>Empty if there is no copy condition or if the copy condition is not parseable.
- */
- abstract ImmutableSet<String> failingAtoms();
-
- private static ApprovalCopyResult create(
- boolean canCopy, ImmutableSet<String> passingAtoms, ImmutableSet<String> failingAtoms) {
- return new AutoValue_ApprovalCopier_ApprovalCopyResult(canCopy, passingAtoms, failingAtoms);
- }
-
- private static ApprovalCopyResult createForMissingCopyCondition() {
- return new AutoValue_ApprovalCopier_ApprovalCopyResult(
- /* canCopy= */ false,
- /* passingAtoms= */ ImmutableSet.of(),
- /* failingAtoms= */ ImmutableSet.of());
- }
-
- private static ApprovalCopyResult createForNonParseableCopyCondition() {
- return new AutoValue_ApprovalCopier_ApprovalCopyResult(
- /* canCopy= */ false,
- /* passingAtoms= */ ImmutableSet.of(),
- /* failingAtoms= */ ImmutableSet.of());
- }
- }
}
diff --git a/java/com/google/gerrit/server/approval/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
index 04683e8..59959ed 100644
--- a/java/com/google/gerrit/server/approval/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -28,6 +28,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
+import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
@@ -37,6 +38,7 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
+import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
@@ -58,6 +60,7 @@
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.approval.ApprovalCopier.ApprovalCopyResult;
import com.google.gerrit.server.change.LabelNormalizer;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -458,10 +461,8 @@
changeUpdate.addToPlannedAttentionSetUpdates(updates);
}
- public Optional<String> formatApprovalCopierResult(
- ApprovalCopier.Result approvalCopierResult, LabelTypes labelTypes) {
+ public Optional<String> formatApprovalCopierResult(ApprovalCopier.Result approvalCopierResult) {
requireNonNull(approvalCopierResult, "approvalCopierResult");
- requireNonNull(labelTypes, "labelTypes");
if (approvalCopierResult.copiedApprovals().isEmpty()
&& approvalCopierResult.outdatedApprovals().isEmpty()) {
@@ -472,17 +473,27 @@
if (!approvalCopierResult.copiedApprovals().isEmpty()) {
message.append("Copied Votes:\n");
- message.append(
- formatApprovalListWithCopyCondition(approvalCopierResult.copiedApprovals(), labelTypes));
+ message.append(formatApprovalListWithCopyCondition(approvalCopierResult.copiedApprovals()));
}
if (!approvalCopierResult.outdatedApprovals().isEmpty()) {
if (!approvalCopierResult.copiedApprovals().isEmpty()) {
message.append("\n");
}
message.append("Outdated Votes:\n");
+ message.append(formatApprovalListWithCopyCondition(approvalCopierResult.outdatedApprovals()));
+ }
+
+ if (Streams.concat(
+ approvalCopierResult.copiedApprovals().stream(),
+ approvalCopierResult.outdatedApprovals().stream())
+ .anyMatch(
+ a ->
+ !Strings.isNullOrEmpty(a.approvalCopyResult().copyEnforcement())
+ || !Strings.isNullOrEmpty(a.approvalCopyResult().copyRestriction()))) {
message.append(
- formatApprovalListWithCopyCondition(
- approvalCopierResult.outdatedApprovals(), labelTypes));
+ "\n"
+ + "\\* The label has `labelCopyEnforcement` or `labelCopyRestriction` configured."
+ + " Only the most relevant condition that determined the outcome is shown.\n");
}
return Optional.of(message.toString());
@@ -511,9 +522,6 @@
* (copy condition: "approverin:7d9e2d5b561e75230e4463ae757ac5d6ff715d85")}
* <li>{@code <comma-separated-list-of-approval-for-the-same-label>} (if no copy condition is
* present), e.g.: {@code Code-Review+1, Code-Review+2}
- * <li>{@code <comma-separated-list-of-approval-for-the-same-label> (label type is missing)} (if
- * the label type is missing), e.g.: {@code Code-Review+1, Code-Review+2 (label type is
- * missing)}
* <li>{@code <comma-separated-list-of-approval-for-the-same-label> (non-parseable copy
* condition: "<non-parseable copy-condition>")} (if a non-parseable copy condition is
* present), e.g.: {@code Code-Review+1, Code-Review+2 (non-parseable copy condition:
@@ -521,12 +529,10 @@
* </ul>
*
* @param approvalDatas the approvals that should be formatted, with approval meta data
- * @param labelTypes the label types
* @return bullet list with the formatted approvals
*/
private String formatApprovalListWithCopyCondition(
- ImmutableSet<ApprovalCopier.Result.PatchSetApprovalData> approvalDatas,
- LabelTypes labelTypes) {
+ ImmutableSet<ApprovalCopier.Result.PatchSetApprovalData> approvalDatas) {
StringBuilder message = new StringBuilder();
// sort approvals by label vote so that we list them in a deterministic order
@@ -547,35 +553,16 @@
for (Map.Entry<String, Collection<ApprovalCopier.Result.PatchSetApprovalData>>
approvalsByLabelEntry : approvalsByLabel.asMap().entrySet()) {
- String label = approvalsByLabelEntry.getKey();
Collection<ApprovalCopier.Result.PatchSetApprovalData> approvalsForSameLabel =
approvalsByLabelEntry.getValue();
- if (!labelTypes.byLabel(label).isPresent()) {
- message
- .append("* ")
- .append(formatApprovalsAsLabelVotesList(approvalsForSameLabel))
- .append(" (label type is missing)\n");
- continue;
- }
-
- LabelType labelType = labelTypes.byLabel(label).get();
- if (!labelType.getCopyCondition().isPresent()) {
- message
- .append("* ")
- .append(formatApprovalsAsLabelVotesList(approvalsForSameLabel))
- .append("\n");
- continue;
- }
-
// Group the approvals that have the same label by the passing atoms. If approvals have the
// same label, but have different passing atoms, we need to list them in separate lines
// (because in each line we will highlight different passing atoms that matched). Approvals
// with the same label and the same passing atoms are formatted as a single line.
ImmutableListMultimap<ImmutableSet<String>, ApprovalCopier.Result.PatchSetApprovalData>
approvalsForSameLabelByPassingAndFailingAtoms =
- Multimaps.index(
- approvalsForSameLabel, ApprovalCopier.Result.PatchSetApprovalData::passingAtoms);
+ Multimaps.index(approvalsForSameLabel, a -> a.approvalCopyResult().passingAtoms());
// Approvals with the same label that have the same passing atoms should have the same failing
// atoms (since the label is the same they have the same copy condition).
@@ -587,7 +574,7 @@
checkThatPropertyIsTheSameForAllApprovals(
approvalsForSameLabelAndSamePassingAtoms,
"failing atoms",
- approvalData -> approvalData.failingAtoms()));
+ approvalData -> approvalData.approvalCopyResult().failingAtoms()));
// The order in which we add lines for approvals with the same label but different passing
// atoms needs to be deterministic for tests. Just sort them by the string representation of
@@ -607,8 +594,7 @@
.append("* ")
.append(
formatApprovalsWithCopyCondition(
- approvalsForSameLabelWithSamePassingAndFailingAtoms,
- labelType.getCopyCondition().get()))
+ approvalsForSameLabelWithSamePassingAndFailingAtoms))
.append("\n");
}
}
@@ -641,13 +627,11 @@
*
* @param approvalsWithSameLabelAndSamePassingAndFailingAtoms the approvals that should be
* formatted, must be for the same label
- * @param copyCondition the copy condition of the label
* @return the formatted approvals
*/
private String formatApprovalsWithCopyCondition(
Collection<ApprovalCopier.Result.PatchSetApprovalData>
- approvalsWithSameLabelAndSamePassingAndFailingAtoms,
- String copyCondition) {
+ approvalsWithSameLabelAndSamePassingAndFailingAtoms) {
// Check that all given approvals have the same label and the same passing and failing atoms.
checkThatPropertyIsTheSameForAllApprovals(
approvalsWithSameLabelAndSamePassingAndFailingAtoms,
@@ -656,11 +640,36 @@
checkThatPropertyIsTheSameForAllApprovals(
approvalsWithSameLabelAndSamePassingAndFailingAtoms,
"passing atoms",
- approvalData -> approvalData.passingAtoms());
+ approvalData -> approvalData.approvalCopyResult().passingAtoms());
checkThatPropertyIsTheSameForAllApprovals(
approvalsWithSameLabelAndSamePassingAndFailingAtoms,
"failing atoms",
- approvalData -> approvalData.failingAtoms());
+ approvalData -> approvalData.approvalCopyResult().failingAtoms());
+
+ ApprovalCopyResult copyResult =
+ !approvalsWithSameLabelAndSamePassingAndFailingAtoms.isEmpty()
+ ? approvalsWithSameLabelAndSamePassingAndFailingAtoms.stream()
+ .findFirst()
+ .get()
+ .approvalCopyResult()
+ : ApprovalCopyResult.createEvaluationSkipped();
+ // In order to keep the message concise and understandable only show most relevant
+ // copy condition and add an asterisk to show if forced conditions are present.
+ String copyConditionName = "";
+ String copyCondition = "";
+ boolean showAsterisk =
+ !Strings.isNullOrEmpty(copyResult.copyEnforcement())
+ || !Strings.isNullOrEmpty(copyResult.copyRestriction());
+ if (copyResult.canCopy() == copyResult.labelCopy()) {
+ copyCondition = copyResult.labelCopyCondition();
+ copyConditionName = "copy condition";
+ } else if (!Strings.isNullOrEmpty(copyResult.copyEnforcement()) && copyResult.forcedCopy()) {
+ copyCondition = copyResult.copyEnforcement();
+ copyConditionName = "forced copy condition";
+ } else if (!Strings.isNullOrEmpty(copyResult.copyRestriction()) && copyResult.forcedNonCopy()) {
+ copyCondition = copyResult.copyRestriction();
+ copyConditionName = "forced copy restriction";
+ }
StringBuilder message = new StringBuilder();
@@ -671,7 +680,10 @@
logger.atWarning().withCause(e).log("Non-parsable query condition");
message.append(
formatApprovalsAsLabelVotesList(approvalsWithSameLabelAndSamePassingAndFailingAtoms));
- message.append(String.format(" (non-parseable copy condition: \"%s\")", copyCondition));
+ message.append(
+ String.format(
+ " (non-parseable %s%s: \"%s\")",
+ copyConditionName, showAsterisk ? "\\*" : "", copyCondition));
return message.toString();
}
@@ -739,14 +751,8 @@
message.append(
formatApprovalsAsLabelVotesList(approvalsWithSameLabelAndSamePassingAndFailingAtoms));
}
- ImmutableSet<String> passingAtoms =
- !approvalsWithSameLabelAndSamePassingAndFailingAtoms.isEmpty()
- ? approvalsWithSameLabelAndSamePassingAndFailingAtoms.iterator().next().passingAtoms()
- : ImmutableSet.of();
message.append(
- String.format(
- " (copy condition: \"%s\")",
- formatCopyConditionAsMarkdown(copyCondition, passingAtoms)));
+ formatApprovalCopyResult(copyResult, copyConditionName, copyCondition, showAsterisk));
return message.toString();
}
@@ -781,6 +787,9 @@
*/
private String formatCopyConditionAsMarkdown(
String copyCondition, ImmutableSet<String> passingAtoms) {
+ if (Strings.isNullOrEmpty(copyCondition)) {
+ return "NEVER";
+ }
StringBuilder formattedCopyCondition = new StringBuilder();
StringTokenizer tokenizer = new StringTokenizer(copyCondition, " ()", /* returnDelims= */ true);
while (tokenizer.hasMoreTokens()) {
@@ -794,7 +803,33 @@
return formattedCopyCondition.toString();
}
+ /**
+ * Formats the given copy condition as a Markdown string.
+ *
+ * <p>Passing atoms are formatted as bold.
+ *
+ * @param approvalCopyResult evaluation information for the copyCondition
+ * @param copyConditionName name by which to refer to the copy condition string
+ * @param copyCondition expression that was evaluated
+ * @param showAsterisk if asterisk referring to forced copy conditions should be shown
+ * @return the formatted copy condition as a Markdown string
+ */
+ private String formatApprovalCopyResult(
+ ApprovalCopyResult approvalCopyResult,
+ String copyConditionName,
+ String copyCondition,
+ boolean showAsterisk) {
+ return String.format(
+ " (%s%s: \"%s\")",
+ copyConditionName,
+ showAsterisk ? "\\*" : "",
+ formatCopyConditionAsMarkdown(copyCondition, approvalCopyResult.passingAtoms()));
+ }
+
private boolean containsUserInPredicate(String copyCondition) throws QueryParseException {
+ if (Strings.isNullOrEmpty(copyCondition)) {
+ return false;
+ }
// Use a request context to run checks as an internal user with expanded visibility. This is
// so that the output of the copy condition does not depend on who is running the current
// request (e.g. a group used in this query might not be visible to the person sending this
diff --git a/java/com/google/gerrit/server/change/ChangeCleanupRunner.java b/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
index 728830c..e3a8b06 100644
--- a/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
+++ b/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
@@ -21,6 +21,7 @@
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.config.ChangeCleanupConfig;
import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.project.LockManager;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.util.ManualRequestContext;
@@ -28,6 +29,7 @@
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
+import java.util.concurrent.locks.Lock;
/** Runnable to enable scheduling change cleanups to run periodically */
public class ChangeCleanupRunner implements Runnable {
@@ -73,6 +75,7 @@
private final OneOffRequestContext oneOffRequestContext;
private final AbandonUtil abandonUtil;
private final RetryHelper retryHelper;
+ private final LockManager lockManager;
private final long abandonAfterMillis;
private final boolean abandonIfMergeable;
@Nullable private final String message;
@@ -82,12 +85,14 @@
OneOffRequestContext oneOffRequestContext,
AbandonUtil abandonUtil,
RetryHelper retryHelper,
+ LockManager lockManager,
@Assisted long abandonAfterMillis,
@Assisted boolean abandonIfMergeable,
@Assisted @Nullable String message) {
this.oneOffRequestContext = oneOffRequestContext;
this.abandonUtil = abandonUtil;
this.retryHelper = retryHelper;
+ this.lockManager = lockManager;
this.abandonAfterMillis = abandonAfterMillis;
this.abandonIfMergeable = abandonIfMergeable;
this.message = message;
@@ -98,10 +103,12 @@
OneOffRequestContext oneOffRequestContext,
AbandonUtil abandonUtil,
RetryHelper retryHelper,
+ LockManager lockManager,
ChangeCleanupConfig cfg) {
this.oneOffRequestContext = oneOffRequestContext;
this.abandonUtil = abandonUtil;
this.retryHelper = retryHelper;
+ this.lockManager = lockManager;
this.abandonAfterMillis = cfg.getAbandonAfter();
this.abandonIfMergeable = cfg.getAbandonIfMergeable();
this.message = cfg.getAbandonMessage();
@@ -109,6 +116,14 @@
@Override
public void run() {
+ Lock lock = lockManager.getLock("change-cleanup");
+ if (!lock.tryLock()) {
+ logger.atInfo().log(
+ "Couldn't acquire change-cleanup lock. Assuming another server is running"
+ + " change-cleanup");
+ return;
+ }
+
logger.atInfo().log("Running change cleanups.");
try (ManualRequestContext ctx = oneOffRequestContext.open()) {
// abandonInactiveOpenChanges skips failures instead of throwing, so retrying will never
@@ -127,6 +142,8 @@
.call();
} catch (RestApiException | UpdateException e) {
logger.atSevere().withCause(e).log("Failed to cleanup changes.");
+ } finally {
+ lock.unlock();
}
}
diff --git a/java/com/google/gerrit/server/change/ChangeInserter.java b/java/com/google/gerrit/server/change/ChangeInserter.java
index ef36bd4..366bf1e 100644
--- a/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -53,6 +53,7 @@
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.ValidationOptionsListener;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.change.ReviewerModifier.InternalReviewerInput;
import com.google.gerrit.server.change.ReviewerModifier.ReviewerModification;
@@ -63,6 +64,8 @@
import com.google.gerrit.server.extensions.events.RevisionCreated;
import com.google.gerrit.server.git.GroupCollector;
import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationInfo;
+import com.google.gerrit.server.git.validators.CommitValidationInfoListener;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.git.validators.TopicValidator;
import com.google.gerrit.server.mail.EmailFactories;
@@ -76,6 +79,7 @@
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.ssh.NoSshInfo;
@@ -127,6 +131,8 @@
private final AutoMerger autoMerger;
private final ChangeUtil changeUtil;
private final DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory;
+ private final PluginSetContext<ValidationOptionsListener> validationOptionsListeners;
+ private final PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners;
private final Change.Id changeId;
private final PatchSet.Id psId;
@@ -137,6 +143,7 @@
private PatchSet.Id cherryPickOf;
private Change.Status status;
private String topic;
+ private PatchSet.Conflicts conflicts;
private String message;
private String patchSetDescription;
private boolean isPrivate;
@@ -145,6 +152,7 @@
private ImmutableListMultimap<String, String> validationOptions = ImmutableListMultimap.of();
private ImmutableMap<String, String> customKeyedValues = ImmutableMap.of();
private boolean validate = true;
+ private ImmutableMap<String, CommitValidationInfo> validationInfos;
private Map<String, Short> approvals;
private RequestScopePropagator requestScopePropagator;
private boolean fireRevisionCreated;
@@ -182,6 +190,8 @@
AutoMerger autoMerger,
ChangeUtil changeUtil,
DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory,
+ PluginSetContext<ValidationOptionsListener> validationOptionsListeners,
+ PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners,
@Assisted Change.Id changeId,
@Assisted ObjectId commitId,
@Assisted String refName) {
@@ -202,6 +212,8 @@
this.autoMerger = autoMerger;
this.changeUtil = changeUtil;
this.diffOperationsForCommitValidationFactory = diffOperationsForCommitValidationFactory;
+ this.validationOptionsListeners = validationOptionsListeners;
+ this.commitValidationInfoListeners = commitValidationInfoListeners;
this.changeId = changeId;
this.psId = PatchSet.id(changeId, INITIAL_PATCH_SET_ID);
@@ -266,6 +278,12 @@
}
@CanIgnoreReturnValue
+ public ChangeInserter setConflicts(PatchSet.Conflicts conflicts) {
+ this.conflicts = conflicts;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
public ChangeInserter setCherryPickOf(PatchSet.Id cherryPickOf) {
this.cherryPickOf = cherryPickOf;
return this;
@@ -283,9 +301,30 @@
return this;
}
+ /**
+ * Disables the commit validation because validation is not needed.
+ *
+ * @return the {@link ChangeInserter} instance to allow chaining calls
+ */
@CanIgnoreReturnValue
- public ChangeInserter setValidate(boolean validate) {
- this.validate = validate;
+ public ChangeInserter disableValidation() {
+ return disableValidation(null);
+ }
+
+ /**
+ * Disables the commit validation because the validation has already been done.
+ *
+ * <p>The result from the validation that has already been done needs to be provided to this
+ * method and is being used to invoke the {@link CommitValidationInfoListener}'s.
+ *
+ * @param validationInfos result of validating {@link #commitId}
+ * @return the {@link ChangeInserter} instance to allow chaining calls
+ */
+ @CanIgnoreReturnValue
+ public ChangeInserter disableValidation(
+ @Nullable ImmutableMap<String, CommitValidationInfo> validationInfos) {
+ this.validate = false;
+ this.validationInfos = validationInfos;
return this;
}
@@ -542,6 +581,11 @@
if (!approvals.isEmpty()) {
update.putReviewer(ctx.getAccountId(), REVIEWER);
}
+
+ if (conflicts != null) {
+ update.setConflicts(conflicts);
+ }
+
if (message != null) {
changeMessage =
cmUtil.setChangeMessage(
@@ -639,23 +683,32 @@
}
private void validate(RepoContext ctx) throws IOException, ResourceConflictException {
- if (!validate) {
- return;
- }
+ try (CommitReceivedEvent event =
+ new CommitReceivedEvent(
+ cmd,
+ projectState.getProject(),
+ change.getDest().branch(),
+ validationOptions,
+ ctx.getRepoView().getConfig(),
+ ctx.getRevWalk().getObjectReader(),
+ commitId,
+ ctx.getIdentifiedUser(),
+ diffOperationsForCommitValidationFactory.create(
+ ctx.getRepoView(), ctx.getInserter()))) {
+ if (!validate) {
+ if (validationInfos != null) {
+ commitValidationInfoListeners.runEach(
+ commitValidationInfoListener ->
+ commitValidationInfoListener.commitValidated(validationInfos, event, psId));
+ }
+ return;
+ }
- try {
- try (CommitReceivedEvent event =
- new CommitReceivedEvent(
- cmd,
- projectState.getProject(),
- change.getDest().branch(),
- validationOptions,
- ctx.getRepoView().getConfig(),
- ctx.getRevWalk().getObjectReader(),
- commitId,
- ctx.getIdentifiedUser(),
- diffOperationsForCommitValidationFactory.create(
- ctx.getRepoView(), ctx.getInserter()))) {
+ try {
+ validationOptionsListeners.runEach(
+ validationOptionsListener ->
+ validationOptionsListener.onPatchSetCreation(
+ change.getDest(), psId, validationOptions));
commitValidatorsFactory
.forGerritCommits(
permissionBackend.user(ctx.getUser()).project(ctx.getProject()),
@@ -664,10 +717,11 @@
new NoSshInfo(),
ctx.getRevWalk(),
change)
+ .patchSet(psId)
.validate(event);
+ } catch (CommitValidationException e) {
+ throw new ResourceConflictException(e.getFullMessage());
}
- } catch (CommitValidationException e) {
- throw new ResourceConflictException(e.getFullMessage());
}
}
diff --git a/java/com/google/gerrit/server/change/ChangeKindCache.java b/java/com/google/gerrit/server/change/ChangeKindCache.java
index 9bd7ad7..3dfed22 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCache.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCache.java
@@ -20,6 +20,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.ChangeKind;
import com.google.gerrit.server.query.change.ChangeData;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevWalk;
@@ -35,11 +36,16 @@
Project.NameKey project,
@Nullable RevWalk rw,
@Nullable Config repoConfig,
+ @Nullable AttributesNodeProvider attributesNodeProvider,
ObjectId prior,
ObjectId next);
ChangeKind getChangeKind(Change change, PatchSet patch);
ChangeKind getChangeKind(
- @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch);
+ @Nullable RevWalk rw,
+ @Nullable Config repoConfig,
+ @Nullable AttributesNodeProvider attributesNodeProvider,
+ ChangeData cd,
+ PatchSet patch);
}
diff --git a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
index 90752c0..b02da0b 100644
--- a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -49,6 +49,7 @@
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.Config;
@@ -81,6 +82,7 @@
public static class NoCache implements ChangeKindCache {
private final boolean useRecursiveMerge;
+ private final boolean useGitattributesForMerge;
private final ChangeData.Factory changeDataFactory;
private final GitRepositoryManager repoManager;
@@ -90,6 +92,7 @@
ChangeData.Factory changeDataFactory,
GitRepositoryManager repoManager) {
this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
+ this.useGitattributesForMerge = MergeUtil.useGitattributesForMerge(serverConfig);
this.changeDataFactory = changeDataFactory;
this.repoManager = repoManager;
}
@@ -99,11 +102,20 @@
Project.NameKey project,
@Nullable RevWalk rw,
@Nullable Config repoConfig,
+ AttributesNodeProvider attributesNodeProvider,
ObjectId prior,
ObjectId next) {
try {
Key key = Key.create(prior, next, useRecursiveMerge);
- return new Loader(key, repoManager, project, rw, repoConfig).call();
+ return new Loader(
+ key,
+ repoManager,
+ project,
+ rw,
+ repoConfig,
+ attributesNodeProvider,
+ useGitattributesForMerge)
+ .call();
} catch (IOException e) {
logger.atWarning().withCause(e).log(
"Cannot check trivial rebase of new patch set %s in %s", next.name(), project);
@@ -118,8 +130,12 @@
@Override
public ChangeKind getChangeKind(
- @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch) {
- return getChangeKindInternal(this, rw, repoConfig, cd, patch);
+ @Nullable RevWalk rw,
+ @Nullable Config repoConfig,
+ AttributesNodeProvider attributesNodeProvider,
+ ChangeData cd,
+ PatchSet patch) {
+ return getChangeKindInternal(this, rw, repoConfig, attributesNodeProvider, cd, patch);
}
}
@@ -170,23 +186,31 @@
private final Project.NameKey projectName;
private final RevWalk alreadyOpenRw;
private final Config repoConfig;
+ private final AttributesNodeProvider repoAttributesNodeProvider;
+ private final boolean useGitattributesForMerge;
private Loader(
Key key,
GitRepositoryManager repoManager,
Project.NameKey projectName,
@Nullable RevWalk rw,
- @Nullable Config repoConfig) {
+ @Nullable Config repoConfig,
+ @Nullable AttributesNodeProvider attributesNodeProvider,
+ boolean useGitattributesForMerge) {
checkArgument(
- (rw == null && repoConfig == null) || (rw != null && repoConfig != null),
- "must either provide both revwalk/config, or neither; got %s/%s",
+ (rw == null && repoConfig == null && attributesNodeProvider == null)
+ || (rw != null && repoConfig != null && attributesNodeProvider != null),
+ "must either provide revwalk/config/attributesNodeProvider, or none; got %s/%s/%s",
rw,
- repoConfig);
+ repoConfig,
+ attributesNodeProvider);
this.key = key;
this.repoManager = repoManager;
this.projectName = projectName;
this.alreadyOpenRw = rw;
this.repoConfig = repoConfig;
+ this.repoAttributesNodeProvider = attributesNodeProvider;
+ this.useGitattributesForMerge = useGitattributesForMerge;
}
@SuppressWarnings("resource") // Resources are manually managed.
@@ -198,11 +222,13 @@
RevWalk rw = alreadyOpenRw;
Config config = repoConfig;
+ AttributesNodeProvider attributesNodeProvider = repoAttributesNodeProvider;
Repository repo = null;
if (alreadyOpenRw == null) {
repo = repoManager.openRepository(projectName);
rw = new RevWalk(repo);
config = repo.getConfig();
+ attributesNodeProvider = repo.createAttributesNodeProvider();
}
try {
RevCommit prior = rw.parseCommit(key.prior());
@@ -233,7 +259,13 @@
// having the same tree as would exist when the prior commit is
// cherry-picked onto the next commit's new first parent.
try (ObjectInserter ins = new InMemoryInserter(rw.getObjectReader())) {
- ThreeWayMerger merger = MergeUtil.newThreeWayMerger(ins, config, key.strategyName());
+ ThreeWayMerger merger =
+ MergeUtil.newThreeWayMerger(
+ ins,
+ config,
+ attributesNodeProvider,
+ key.strategyName(),
+ useGitattributesForMerge);
merger.setBase(prior.getParent(0));
if (merger.merge(next.getParent(0), prior)
&& merger.getResultTreeId().equals(next.getTree())) {
@@ -317,6 +349,7 @@
private final Cache<Key, ChangeKind> cache;
private final boolean useRecursiveMerge;
+ private final boolean useGitattributesForMerge;
private final ChangeData.Factory changeDataFactory;
private final GitRepositoryManager repoManager;
@@ -328,6 +361,7 @@
GitRepositoryManager repoManager) {
this.cache = cache;
this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
+ this.useGitattributesForMerge = MergeUtil.useGitattributesForMerge(serverConfig);
this.changeDataFactory = changeDataFactory;
this.repoManager = repoManager;
}
@@ -337,11 +371,22 @@
Project.NameKey project,
@Nullable RevWalk rw,
@Nullable Config repoConfig,
+ @Nullable AttributesNodeProvider attributesNodeProvider,
ObjectId prior,
ObjectId next) {
try {
Key key = Key.create(prior, next, useRecursiveMerge);
- ChangeKind kind = cache.get(key, new Loader(key, repoManager, project, rw, repoConfig));
+ ChangeKind kind =
+ cache.get(
+ key,
+ new Loader(
+ key,
+ repoManager,
+ project,
+ rw,
+ repoConfig,
+ attributesNodeProvider,
+ useGitattributesForMerge));
logger.atFine().log("Change kind of new patch set %s in %s: %s", next.name(), project, kind);
return kind;
} catch (ExecutionException e) {
@@ -358,14 +403,19 @@
@Override
public ChangeKind getChangeKind(
- @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch) {
- return getChangeKindInternal(this, rw, repoConfig, cd, patch);
+ @Nullable RevWalk rw,
+ @Nullable Config repoConfig,
+ @Nullable AttributesNodeProvider attributesNodeProvider,
+ ChangeData cd,
+ PatchSet patch) {
+ return getChangeKindInternal(this, rw, repoConfig, attributesNodeProvider, cd, patch);
}
private static ChangeKind getChangeKindInternal(
ChangeKindCache cache,
@Nullable RevWalk rw,
@Nullable Config repoConfig,
+ AttributesNodeProvider attributesNodeProvider,
ChangeData change,
PatchSet patch) {
ChangeKind kind = ChangeKind.REWORK;
@@ -390,7 +440,12 @@
if (priorPs != patch) {
kind =
cache.getChangeKind(
- change.project(), rw, repoConfig, priorPs.commitId(), patch.commitId());
+ change.project(),
+ rw,
+ repoConfig,
+ attributesNodeProvider,
+ priorPs.commitId(),
+ patch.commitId());
}
} catch (StorageException e) {
// Do nothing; assume we have a complex change
@@ -419,7 +474,12 @@
RevWalk rw = new RevWalk(repo)) {
kind =
getChangeKindInternal(
- cache, rw, repo.getConfig(), changeDataFactory.create(change), patch);
+ cache,
+ rw,
+ repo.getConfig(),
+ repo.createAttributesNodeProvider(),
+ changeDataFactory.create(change),
+ patch);
} catch (IOException e) {
// Do nothing; assume we have a complex change
logger.atWarning().withCause(e).log(
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 9f7a7fc..b8926d0 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -575,7 +575,7 @@
bu.addOp(
notes.getChangeId(),
inserter
- .setValidate(false)
+ .disableValidation()
.setFireRevisionCreated(false)
.setAllowClosed(true)
.setMessage("Patch set for merged commit inserted by consistency checker"));
diff --git a/java/com/google/gerrit/server/change/MergeabilityCache.java b/java/com/google/gerrit/server/change/MergeabilityCache.java
index b432bc9..f3f718b 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCache.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCache.java
@@ -22,24 +22,6 @@
/** Cache for mergeability of commits into destination branches. */
public interface MergeabilityCache {
- class NotImplemented implements MergeabilityCache {
- @Override
- public boolean get(
- ObjectId commit,
- Ref intoRef,
- SubmitType submitType,
- String mergeStrategy,
- BranchNameKey dest,
- Repository repo) {
- throw new UnsupportedOperationException("Mergeability checking disabled");
- }
-
- @Override
- public Boolean getIfPresent(
- ObjectId commit, Ref intoRef, SubmitType submitType, String mergeStrategy) {
- throw new UnsupportedOperationException("Mergeability checking disabled");
- }
- }
boolean get(
ObjectId commit,
diff --git a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index 44af1e4..7097eb8 100644
--- a/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -221,6 +221,9 @@
@Override
public Boolean getIfPresent(
ObjectId commit, Ref intoRef, SubmitType submitType, String mergeStrategy) {
- return cache.getIfPresent(new EntryKey(commit, toId(intoRef), submitType, mergeStrategy));
+ EntryKey entryKey = new EntryKey(commit, toId(intoRef), submitType, mergeStrategy);
+ Boolean mergeable = cache.getIfPresent(entryKey);
+ logger.atFine().log("got mergeable=%s (entryKey=%s)", mergeable, entryKey);
+ return mergeable;
}
}
diff --git a/java/com/google/gerrit/server/change/PatchSetInserter.java b/java/com/google/gerrit/server/change/PatchSetInserter.java
index 3b0f6fb..797b84a 100644
--- a/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -22,6 +22,7 @@
import static java.util.Objects.requireNonNull;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.Nullable;
@@ -37,12 +38,15 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.ValidationOptionsListener;
import com.google.gerrit.server.approval.ApprovalCopier;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.extensions.events.RevisionCreated;
import com.google.gerrit.server.extensions.events.WorkInProgressStateChanged;
import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationInfo;
+import com.google.gerrit.server.git.validators.CommitValidationInfoListener;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.git.validators.TopicValidator;
import com.google.gerrit.server.notedb.ChangeNotes;
@@ -53,6 +57,7 @@
import com.google.gerrit.server.permissions.ChangePermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.ssh.NoSshInfo;
import com.google.gerrit.server.update.BatchUpdateOp;
@@ -89,6 +94,8 @@
private final AutoMerger autoMerger;
private final TopicValidator topicValidator;
private final DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory;
+ private final PluginSetContext<ValidationOptionsListener> validationOptionsListeners;
+ private final PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners;
// Assisted-injected fields.
private final PatchSet.Id psId;
@@ -103,6 +110,7 @@
private String description;
private Boolean workInProgress;
private boolean validate = true;
+ private ImmutableMap<String, CommitValidationInfo> validationInfos;
private boolean checkAddPatchSetPermission = true;
private List<String> groups = Collections.emptyList();
private ImmutableListMultimap<String, String> validationOptions = ImmutableListMultimap.of();
@@ -110,6 +118,7 @@
private boolean allowClosed;
private boolean sendEmail = true;
private String topic;
+ private PatchSet.Conflicts conflicts;
private boolean storeCopiedVotes = true;
// Fields set during some phase of BatchUpdate.Op.
@@ -139,6 +148,8 @@
AutoMerger autoMerger,
TopicValidator topicValidator,
DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory,
+ PluginSetContext<ValidationOptionsListener> validationOptionsListeners,
+ PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners,
@Assisted ChangeNotes notes,
@Assisted PatchSet.Id psId,
@Assisted ObjectId commitId) {
@@ -156,6 +167,8 @@
this.autoMerger = autoMerger;
this.topicValidator = topicValidator;
this.diffOperationsForCommitValidationFactory = diffOperationsForCommitValidationFactory;
+ this.validationOptionsListeners = validationOptionsListeners;
+ this.commitValidationInfoListeners = commitValidationInfoListeners;
this.origNotes = notes;
this.psId = psId;
@@ -184,9 +197,30 @@
return this;
}
+ /**
+ * Disables the commit validation because validation is not needed.
+ *
+ * @return the {@link PatchSetInserter} instance to allow chaining calls
+ */
@CanIgnoreReturnValue
- public PatchSetInserter setValidate(boolean validate) {
- this.validate = validate;
+ public PatchSetInserter disableValidation() {
+ return disableValidation(null);
+ }
+
+ /**
+ * Disables the commit validation because the validation has already been done.
+ *
+ * <p>The result from the validation that has already been done needs to be provided to this
+ * method and is being used to invoke the {@link CommitValidationInfoListener}'s.
+ *
+ * @param validationInfos result of validating {@link #commitId}
+ * @return the {@link PatchSetInserter} instance to allow chaining calls
+ */
+ @CanIgnoreReturnValue
+ public PatchSetInserter disableValidation(
+ @Nullable ImmutableMap<String, CommitValidationInfo> validationInfos) {
+ this.validate = false;
+ this.validationInfos = validationInfos;
return this;
}
@@ -235,6 +269,12 @@
return this;
}
+ @CanIgnoreReturnValue
+ public PatchSetInserter setConflicts(PatchSet.Conflicts conflicts) {
+ this.conflicts = conflicts;
+ return this;
+ }
+
/**
* We always want to store copied votes except when the change is getting submitted and a new
* patch-set is created on submit (using submit strategies such as "REBASE_ALWAYS"). In such
@@ -268,6 +308,7 @@
ctx.getProject(),
ctx.getRevWalk(),
ctx.getRepoView().getConfig(),
+ ctx.getRepoView().getAttributesNodeProvider(),
psUtil.current(origNotes).commitId(),
commitId);
@@ -337,13 +378,17 @@
ctx.getNotes(), patchSet, ctx.getRepoView(), update);
}
- mailMessage = insertChangeMessage(update, ctx);
+ if (conflicts != null) {
+ update.setConflicts(conflicts);
+ }
+
+ mailMessage = insertChangeMessage(update);
return true;
}
@Nullable
- private String insertChangeMessage(ChangeUpdate update, ChangeContext ctx) {
+ private String insertChangeMessage(ChangeUpdate update) {
StringBuilder messageBuilder = new StringBuilder();
if (message != null) {
messageBuilder.append(message);
@@ -351,12 +396,7 @@
if (approvalCopierResult != null) {
approvalsUtil
- .formatApprovalCopierResult(
- approvalCopierResult,
- projectCache
- .get(ctx.getProject())
- .orElseThrow(illegalState(ctx.getProject()))
- .getLabelTypes())
+ .formatApprovalCopierResult(approvalCopierResult)
.ifPresent(
msg -> {
if (message != null && !message.endsWith("\n")) {
@@ -418,9 +458,6 @@
.get(ctx.getProject())
.orElseThrow(illegalState(ctx.getProject()))
.checkStatePermitsWrite();
- if (!validate) {
- return;
- }
String refName = getPatchSetId().toRefName();
try (CommitReceivedEvent event =
@@ -441,17 +478,33 @@
ctx.getIdentifiedUser(),
diffOperationsForCommitValidationFactory.create(
ctx.getRepoView(), ctx.getInserter()))) {
- commitValidatorsFactory
- .forGerritCommits(
- permissionBackend.user(ctx.getUser()).project(ctx.getProject()),
- origNotes.getChange().getDest(),
- ctx.getIdentifiedUser(),
- new NoSshInfo(),
- ctx.getRevWalk(),
- origNotes.getChange())
- .validate(event);
- } catch (CommitValidationException e) {
- throw new ResourceConflictException(e.getFullMessage());
+ if (!validate) {
+ if (validationInfos != null) {
+ commitValidationInfoListeners.runEach(
+ commitValidationInfoListener ->
+ commitValidationInfoListener.commitValidated(validationInfos, event, psId));
+ }
+ return;
+ }
+
+ validationOptionsListeners.runEach(
+ validationOptionsListener ->
+ validationOptionsListener.onPatchSetCreation(
+ change.getDest(), psId, validationOptions));
+ try {
+ commitValidatorsFactory
+ .forGerritCommits(
+ permissionBackend.user(ctx.getUser()).project(ctx.getProject()),
+ origNotes.getChange().getDest(),
+ ctx.getIdentifiedUser(),
+ new NoSshInfo(),
+ ctx.getRevWalk(),
+ origNotes.getChange())
+ .patchSet(psId)
+ .validate(event);
+ } catch (CommitValidationException e) {
+ throw new ResourceConflictException(e.getFullMessage());
+ }
}
}
}
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index eeaa161..d6a067f 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -124,6 +124,7 @@
private String mergeStrategy;
private boolean verifyNeedsRebase = true;
private final boolean useDiff3;
+ private final boolean useGitattributesForMerge;
private CodeReviewCommit rebasedCommit;
private PatchSet.Id rebasedPatchSetId;
@@ -208,6 +209,7 @@
this.projectName = notes.getProjectName();
this.originalPatchSet = originalPatchSet;
this.useDiff3 = cfg.getBoolean("change", null, "diff3ConflictView", false);
+ this.useGitattributesForMerge = MergeUtil.useGitattributesForMerge(cfg);
}
@CanIgnoreReturnValue
@@ -362,11 +364,15 @@
.setDescription("Rebase")
.setFireRevisionCreated(fireRevisionCreated)
.setCheckAddPatchSetPermission(checkAddPatchSetPermission)
- .setValidate(validate)
.setSendEmail(sendEmail)
// The votes are automatically copied and they don't count as copied votes. See
// method's javadoc.
.setStoreCopiedVotes(storeCopiedVotes);
+ rebasedCommit.getConflicts().ifPresent(patchSetInserter::setConflicts);
+
+ if (!validate) {
+ patchSetInserter.disableValidation();
+ }
if (!rebasedCommit.getFilesWithGitConflicts().isEmpty()
&& !notes.getChange().isWorkInProgress()) {
@@ -496,10 +502,19 @@
}
DirCache dc = DirCache.newInCore();
- if (allowConflicts && merger instanceof ResolveMerger) {
- // The DirCache must be set on ResolveMerger before calling
- // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get populated.
- ((ResolveMerger) merger).setDirCache(dc);
+ if (merger instanceof ResolveMerger) {
+ if (useGitattributesForMerge) {
+ // We need to set the attributes provider before attempting the merge in order to read and
+ // honor gitattributes merge settings correctly
+ ((ResolveMerger) merger)
+ .setAttributesNodeProvider(ctx.getRepoView().getAttributesNodeProvider());
+ }
+ if (allowConflicts) {
+ // The DirCache must be set on ResolveMerger before calling
+ // ResolveMerger#merge(AnyObjectId...) otherwise the entries in DirCache don't get
+ // populated.
+ ((ResolveMerger) merger).setDirCache(dc);
+ }
}
boolean success = merger.merge(original, base);
@@ -605,7 +620,7 @@
}
ObjectId objectId = ctx.getInserter().insert(cb);
CodeReviewCommit commit = ((CodeReviewRevWalk) ctx.getRevWalk()).parseCommit(objectId);
- commit.setFilesWithGitConflicts(filesWithGitConflicts);
+ commit.setConflicts(original, base, filesWithGitConflicts);
logger.atFine().log("rebased commit=%s", commit.name());
return commit;
}
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index 5b63fac..d6cbeda 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -42,6 +42,7 @@
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.CommitInfo;
+import com.google.gerrit.extensions.common.ConflictsInfo;
import com.google.gerrit.extensions.common.FetchInfo;
import com.google.gerrit.extensions.common.PushCertificateInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
@@ -85,6 +86,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
@@ -187,7 +189,16 @@
AccountLoader accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
try (Repository repo = openRepoIfNecessary(cd.project());
RevWalk rw = newRevWalk(repo)) {
- RevisionInfo rev = toRevisionInfo(accountLoader, cd, in, repo, rw, true, null);
+ RevisionInfo rev =
+ toRevisionInfo(
+ accountLoader,
+ cd,
+ in,
+ repo,
+ rw,
+ true,
+ null,
+ repo != null ? repo.createAttributesNodeProvider() : null);
accountLoader.fill();
return rev;
}
@@ -267,6 +278,8 @@
Map<String, RevisionInfo> res = new LinkedHashMap<>();
try (Repository repo = openRepoIfNecessary(cd.project());
RevWalk rw = newRevWalk(repo)) {
+ AttributesNodeProvider attributesNodeProvider =
+ repo != null ? repo.createAttributesNodeProvider() : null;
for (PatchSet in : map.values()) {
PatchSet.Id id = in.id();
boolean want;
@@ -280,7 +293,8 @@
if (want) {
res.put(
in.commitId().name(),
- toRevisionInfo(accountLoader, cd, in, repo, rw, false, changeInfo));
+ toRevisionInfo(
+ accountLoader, cd, in, repo, rw, false, changeInfo, attributesNodeProvider));
}
}
return res;
@@ -325,7 +339,8 @@
@Nullable Repository repo,
@Nullable RevWalk rw,
boolean fillCommit,
- @Nullable ChangeInfo changeInfo)
+ @Nullable ChangeInfo changeInfo,
+ @Nullable AttributesNodeProvider attributesNodeProvider)
throws PatchListNotAvailableException, GpgException, IOException, PermissionBackendException {
Change c = cd.change();
RevisionInfo out = new RevisionInfo();
@@ -345,8 +360,21 @@
out.realUploader = accountLoader.get(in.realUploader());
}
out.fetch = makeFetchMap(cd, in);
- out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in);
+ out.kind =
+ changeKindCache.getChangeKind(
+ rw, repo != null ? repo.getConfig() : null, attributesNodeProvider, cd, in);
out.description = in.description().orElse(null);
+ out.conflicts =
+ in.conflicts()
+ .map(
+ conflicts -> {
+ ConflictsInfo conflictsInfo = new ConflictsInfo();
+ conflictsInfo.containsConflicts = conflicts.containsConflicts();
+ conflictsInfo.ours = conflicts.ours().map(ObjectId::getName).orElse(null);
+ conflictsInfo.theirs = conflicts.theirs().map(ObjectId::getName).orElse(null);
+ return conflictsInfo;
+ })
+ .orElse(null);
boolean setCommit = has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT));
boolean addFooters = out.isCurrent && has(COMMIT_FOOTERS);
diff --git a/java/com/google/gerrit/server/config/AccountConfig.java b/java/com/google/gerrit/server/config/AccountConfig.java
new file mode 100644
index 0000000..4e852bc
--- /dev/null
+++ b/java/com/google/gerrit/server/config/AccountConfig.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2024 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.config;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.eclipse.jgit.lib.Config;
+
+/** Account related settings from {@code gerrit.config}. */
+@Singleton
+public class AccountConfig {
+ private final String[] caseInsensitiveLocalParts;
+
+ @Inject
+ AccountConfig(@GerritServerConfig Config cfg) {
+ caseInsensitiveLocalParts = cfg.getStringList("accounts", null, "caseInsensitiveLocalPart");
+ }
+
+ public String[] getCaseInsensitiveLocalParts() {
+ return caseInsensitiveLocalParts;
+ }
+}
diff --git a/java/com/google/gerrit/server/config/CapabilityConstants.java b/java/com/google/gerrit/server/config/CapabilityConstants.java
index 1f799c6..43c4933 100644
--- a/java/com/google/gerrit/server/config/CapabilityConstants.java
+++ b/java/com/google/gerrit/server/config/CapabilityConstants.java
@@ -27,6 +27,7 @@
public String batchChangesLimit;
public String createAccount;
public String createGroup;
+ public String deleteGroup;
public String createProject;
public String emailReviewers;
public String flushCaches;
diff --git a/java/com/google/gerrit/server/config/ConfigUtil.java b/java/com/google/gerrit/server/config/ConfigUtil.java
index e76207c..121c62e 100644
--- a/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -371,8 +371,12 @@
} else if (isLong(t)) {
f.set(s, cfg.getLong(section, sub, n, (Long) d));
} else if (isBoolean(t)) {
+ // Sets the field if:
+ // - 'cfg' value is 'true'.
+ // - the default value is 'true'.
+ // - i is set.
boolean b = cfg.getBoolean(section, sub, n, (Boolean) d);
- if (b || i != null) {
+ if (b || (Boolean) d || i != null) {
f.set(s, b);
}
} else if (t.isEnum()) {
@@ -427,10 +431,13 @@
requireNonNull(val, "Default cannot be null for: " + n);
}
}
- if (!isBoolean(t) || (boolean) val) {
+ if (!isBoolean(t) || (boolean) val || (Boolean) f.get(defaults)) {
// To reproduce the same behavior as in the loadSection method above, values are
// explicitly set for all types, except the boolean type. For the boolean type, the value
- // is set only if it is 'true' (so, the false value is omitted in the result object).
+ // is set only in the following cases:
+ // - 'cfg' value is 'true'.
+ // - the default value is 'true'.
+ // Otherwise, false values are omitted in the result object.
f.set(s, val);
}
}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 1f0bd6e..e1b3317 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -93,6 +93,7 @@
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.ServerStateProvider;
import com.google.gerrit.server.TraceRequestListener;
+import com.google.gerrit.server.ValidationOptionsListener;
import com.google.gerrit.server.account.AccountControl;
import com.google.gerrit.server.account.AccountDeactivator;
import com.google.gerrit.server.account.AccountExternalIdCreator;
@@ -148,10 +149,10 @@
import com.google.gerrit.server.git.validators.CommentCountValidator;
import com.google.gerrit.server.git.validators.CommentCumulativeSizeValidator;
import com.google.gerrit.server.git.validators.CommentSizeValidator;
+import com.google.gerrit.server.git.validators.CommitValidationInfoListener;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.MergeValidationListener;
import com.google.gerrit.server.git.validators.MergeValidators;
-import com.google.gerrit.server.git.validators.MergeValidators.AccountMergeValidator;
import com.google.gerrit.server.git.validators.MergeValidators.GroupMergeValidator;
import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator;
import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
@@ -196,6 +197,7 @@
import com.google.gerrit.server.project.SubmitRequirementsEvaluatorImpl;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.approval.ApprovalModule;
+import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeIsVisibleToPredicate;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
@@ -410,6 +412,7 @@
DynamicSet.bind(binder(), CommitValidationListener.class)
.to(SubmitRequirementConfigValidator.class);
DynamicSet.bind(binder(), CommitValidationListener.class).to(PrologRulesWarningValidator.class);
+ DynamicSet.setOf(binder(), CommitValidationInfoListener.class);
DynamicSet.setOf(binder(), CommentValidator.class);
DynamicSet.setOf(binder(), DiffValidator.class);
DynamicSet.bind(binder(), DiffValidator.class).to(DiffFileSizeValidator.class);
@@ -469,6 +472,7 @@
DynamicSet.setOf(binder(), AccountStateProvider.class);
DynamicMap.mapOf(binder(), AccountTagProvider.class);
DynamicSet.setOf(binder(), AttentionSetListener.class);
+ DynamicSet.setOf(binder(), ValidationOptionsListener.class);
DynamicMap.mapOf(binder(), MailFilter.class);
bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);
@@ -493,6 +497,7 @@
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class);
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeHasOperandFactory.class);
DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeIsOperandFactory.class);
+ DynamicMap.mapOf(binder(), ApprovalQueryBuilder.UserInOperandFactory.class);
DynamicSet.setOf(binder(), ChangePluginDefinedInfoFactory.class);
install(new GitwebConfig.LegacyModule(cfg));
@@ -500,7 +505,6 @@
bind(AnonymousUser.class);
factory(AbandonOp.Factory.class);
- factory(AccountMergeValidator.Factory.class);
factory(GroupMergeValidator.Factory.class);
factory(RefOperationValidators.Factory.class);
factory(OnSubmitValidators.Factory.class);
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index 1b8e162..be55ec1 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -685,7 +685,7 @@
new PersonIdent(currentEditCommit.getCommitterIdent(), timestamp));
CodeReviewCommit newEditCommit = revWalk.parseCommit(newEditCommitId);
- newEditCommit.setFilesWithGitConflicts(filesWithGitConflicts);
+ newEditCommit.setConflicts(basePatchSetCommit, editCommitId, filesWithGitConflicts);
return newEditCommit;
}
}
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 0189306..f962bb8 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -183,7 +183,13 @@
// Previously checked that the base patch set is the current patch set.
ObjectId prior = basePatchSet.commitId();
ChangeKind kind =
- changeKindCache.getChangeKind(change.getProject(), rw, repo.getConfig(), prior, squashed);
+ changeKindCache.getChangeKind(
+ change.getProject(),
+ rw,
+ repo.getConfig(),
+ repo.createAttributesNodeProvider(),
+ prior,
+ squashed);
if (kind == ChangeKind.NO_CODE_CHANGE) {
message.append("Commit message was updated.");
inserter.setDescription("Edit commit message");
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index d6562a6..6380db3 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -76,6 +76,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
@@ -361,6 +362,7 @@
public void addPatchSets(
RevWalk revWalk,
Config repoConfig,
+ AttributesNodeProvider attributesNodeProvider,
ChangeAttribute ca,
Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
boolean includeFiles,
@@ -370,7 +372,8 @@
ca.patchSets = new ArrayList<>(changeData.patchSets().size());
for (PatchSet p : changeData.patchSets()) {
PatchSetAttribute psa =
- asPatchSetAttribute(revWalk, repoConfig, changeData, p, accountLoader);
+ asPatchSetAttribute(
+ revWalk, repoConfig, attributesNodeProvider, changeData, p, accountLoader);
if (approvals != null) {
addApprovals(psa, p.id(), approvals, changeData.getLabelTypes(), accountLoader);
}
@@ -434,14 +437,20 @@
}
public PatchSetAttribute asPatchSetAttribute(
- RevWalk revWalk, Config repoConfig, ChangeData changeData, PatchSet patchSet) {
- return asPatchSetAttribute(revWalk, repoConfig, changeData, patchSet, null);
+ RevWalk revWalk,
+ Config repoConfig,
+ AttributesNodeProvider attributesNodeProvider,
+ ChangeData changeData,
+ PatchSet patchSet) {
+ return asPatchSetAttribute(
+ revWalk, repoConfig, attributesNodeProvider, changeData, patchSet, null);
}
/** Create a PatchSetAttribute for the given patchset suitable for serialization to JSON. */
public PatchSetAttribute asPatchSetAttribute(
RevWalk revWalk,
Config repoConfig,
+ AttributesNodeProvider attributesNodeProvider,
ChangeData changeData,
PatchSet patchSet,
AccountAttributeLoader accountLoader) {
@@ -476,7 +485,9 @@
p.sizeDeletions += fileDiff.deletions();
p.sizeInsertions += fileDiff.insertions();
}
- p.kind = changeKindCache.getChangeKind(revWalk, repoConfig, changeData, patchSet);
+ p.kind =
+ changeKindCache.getChangeKind(
+ revWalk, repoConfig, attributesNodeProvider, changeData, patchSet);
} catch (IOException | StorageException e) {
logger.atSevere().withCause(e).log("Cannot load patch set data for %s", patchSet.id());
} catch (DiffNotAvailableException e) {
diff --git a/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
index 66e894c..125c47f 100644
--- a/java/com/google/gerrit/server/events/StreamEventsApiListener.java
+++ b/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -216,7 +216,11 @@
try (Repository repo = repoManager.openRepository(changeData.change().getProject());
RevWalk revWalk = new RevWalk(repo)) {
return eventFactory.asPatchSetAttribute(
- revWalk, repo.getConfig(), changeData, patchSet);
+ revWalk,
+ repo.getConfig(),
+ repo.createAttributesNodeProvider(),
+ changeData,
+ patchSet);
} catch (IOException e) {
throw new RuntimeException(e);
}
diff --git a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
index cd91745..7ad9f85 100644
--- a/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
+++ b/java/com/google/gerrit/server/experiments/ExperimentFeaturesConstants.java
@@ -60,4 +60,8 @@
/** Whether we allow fix suggestions in HumanComments. */
public static final String ALLOW_FIX_SUGGESTIONS_IN_COMMENTS =
"GerritBackendFeature__allow_fix_suggestions_in_comments";
+
+ /** Whether we allow fix suggestions in HumanComments. */
+ public static final String ENABLE_CENTRAL_OVERRIDE_FOR_CODE_REVIEW_COPY_CONDITION =
+ "GerritBackendFeature__enable_central_override_for_code_review_copy_condition";
}
diff --git a/java/com/google/gerrit/server/git/CodeReviewCommit.java b/java/com/google/gerrit/server/git/CodeReviewCommit.java
index 79df21a..2990aa8 100644
--- a/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -32,6 +32,7 @@
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
@@ -134,6 +135,16 @@
*/
private transient Optional<String> statusMessage = Optional.empty();
+ /**
+ * Information about conflicts in this commit.
+ *
+ * <p>Only set for patch sets that are created by Gerrit as a result of performing a Git merge.
+ *
+ * <p>If this field is not set it's unknown whether this patch set contains any file with
+ * conflicts.
+ */
+ @Nullable private PatchSet.Conflicts conflicts;
+
/** List of files in this commit that contain Git conflict markers. */
private ImmutableSet<String> filesWithGitConflicts;
@@ -161,15 +172,32 @@
this.statusMessage = Optional.ofNullable(statusMessage);
}
- public ImmutableSet<String> getFilesWithGitConflicts() {
- return filesWithGitConflicts != null ? filesWithGitConflicts : ImmutableSet.of();
+ public void setNoConflicts() {
+ this.conflicts =
+ PatchSet.Conflicts.create(
+ Optional.empty(), Optional.empty(), /* containsConflicts= */ false);
}
- public void setFilesWithGitConflicts(@Nullable Set<String> filesWithGitConflicts) {
- this.filesWithGitConflicts =
- filesWithGitConflicts != null && !filesWithGitConflicts.isEmpty()
- ? ImmutableSet.copyOf(filesWithGitConflicts)
- : null;
+ public void setConflicts(
+ ObjectId ours, ObjectId theirs, @Nullable Set<String> filesWithGitConflicts) {
+ if (filesWithGitConflicts != null && !filesWithGitConflicts.isEmpty()) {
+ this.conflicts =
+ PatchSet.Conflicts.create(
+ Optional.of(ours), Optional.of(theirs), /* containsConflicts= */ true);
+ this.filesWithGitConflicts = ImmutableSet.copyOf(filesWithGitConflicts);
+ } else {
+ this.conflicts =
+ PatchSet.Conflicts.create(
+ Optional.of(ours), Optional.of(theirs), /* containsConflicts= */ false);
+ }
+ }
+
+ public Optional<PatchSet.Conflicts> getConflicts() {
+ return Optional.ofNullable(conflicts);
+ }
+
+ public ImmutableSet<String> getFilesWithGitConflicts() {
+ return filesWithGitConflicts != null ? filesWithGitConflicts : ImmutableSet.of();
}
public PatchSet.Id getPatchsetId() {
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index 4a5e1b0..30f2ee8 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -46,6 +46,7 @@
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.ValidationOptionsUtil;
import com.google.gerrit.server.extensions.events.ChangeReverted;
+import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.mail.EmailFactories;
import com.google.gerrit.server.mail.send.ChangeEmail;
import com.google.gerrit.server.mail.send.MessageIdGenerator;
@@ -173,12 +174,12 @@
try (Repository git = repoManager.openRepository(notes.getProjectName());
ObjectInserter oi = git.newObjectInserter();
ObjectReader reader = oi.newReader();
- RevWalk revWalk = new RevWalk(reader)) {
+ CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader)) {
ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
- ObjectId revCommit =
+ CodeReviewCommit revertCommit =
createRevertCommit(message, notes, user, timestamp, oi, revWalk, generatedChangeId);
return createRevertChangeFromCommit(
- revCommit, input, notes, user, generatedChangeId, timestamp, oi, revWalk, git);
+ revertCommit, input, notes, user, generatedChangeId, timestamp, oi, revWalk, git);
} catch (RepositoryNotFoundException e) {
throw new ResourceNotFoundException(notes.getChangeId().toString(), e);
}
@@ -192,16 +193,16 @@
* @param notes ChangeNotes of the change being reverted.
* @param user Current User performing the revert.
* @param ts Timestamp of creation for the commit.
- * @return ObjectId that represents the newly created commit.
+ * @return that newly created revert commit.
*/
- public ObjectId createRevertCommit(
+ public CodeReviewCommit createRevertCommit(
String message, ChangeNotes notes, CurrentUser user, Instant ts)
throws RestApiException, IOException {
try (Repository git = repoManager.openRepository(notes.getProjectName());
ObjectInserter oi = git.newObjectInserter();
ObjectReader reader = oi.newReader();
- RevWalk revWalk = new RevWalk(reader)) {
+ CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader)) {
return createRevertCommit(message, notes, user, ts, oi, revWalk, null);
} catch (RepositoryNotFoundException e) {
throw new ResourceNotFoundException(notes.getProjectName().toString(), e);
@@ -256,13 +257,13 @@
* @throws ResourceConflictException Can't revert the initial commit.
* @throws IOException Thrown in case of I/O errors.
*/
- private ObjectId createRevertCommit(
+ private CodeReviewCommit createRevertCommit(
String message,
ChangeNotes notes,
CurrentUser user,
Instant ts,
ObjectInserter oi,
- RevWalk revWalk,
+ CodeReviewRevWalk revWalk,
@Nullable ObjectId generatedChangeId)
throws ResourceConflictException, IOException {
@@ -293,17 +294,26 @@
message = ChangeIdUtil.insertId(message, generatedChangeId, true);
}
- return createCommitWithTree(
- oi,
- authorIdent,
- committerIdent,
- ImmutableList.of(commitToRevert),
- message,
- parentToCommitToRevert.getTree());
+ CodeReviewCommit revertCommit =
+ revWalk.parseCommit(
+ createCommitWithTree(
+ oi,
+ authorIdent,
+ committerIdent,
+ ImmutableList.of(commitToRevert),
+ message,
+ parentToCommitToRevert.getTree()));
+
+ // The revert commit is based on the commit that is being reverted and has the same tree as the
+ // parent of the commit that is being reverted. This means revert commit never contains any
+ // conflicts.
+ revertCommit.setNoConflicts();
+
+ return revertCommit;
}
private Change.Id createRevertChangeFromCommit(
- ObjectId revertCommitId,
+ CodeReviewCommit revertCommit,
RevertInput input,
ChangeNotes notes,
CurrentUser user,
@@ -313,7 +323,6 @@
RevWalk revWalk,
Repository git)
throws IOException, RestApiException, UpdateException, ConfigInvalidException {
- RevCommit revertCommit = revWalk.parseCommit(revertCommitId);
Change.Id changeId = Change.id(seq.nextChangeId());
if (input.getWorkInProgress()) {
input.notify = firstNonNull(input.notify, NotifyHandling.NONE);
@@ -341,6 +350,7 @@
ins.setReviewersAndCcsIgnoreVisibility(reviewers, ccs);
ins.setRevertOf(notes.getChangeId());
ins.setWorkInProgress(input.getWorkInProgress());
+ revertCommit.getConflicts().ifPresent(ins::setConflicts);
try (BatchUpdate bu = updateFactory.create(notes.getProjectName(), user, ts)) {
bu.setRepository(git, revWalk, oi);
diff --git a/java/com/google/gerrit/server/git/MergeUtil.java b/java/com/google/gerrit/server/git/MergeUtil.java
index b38d46e..f4553a8 100644
--- a/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/java/com/google/gerrit/server/git/MergeUtil.java
@@ -75,6 +75,7 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
import org.eclipse.jgit.diff.Sequence;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
@@ -133,6 +134,10 @@
return cfg.getBoolean("core", null, "useRecursiveMerge", true);
}
+ public static boolean useGitattributesForMerge(Config cfg) {
+ return cfg.getBoolean("core", null, "useGitattributesForMerge", false);
+ }
+
public static ThreeWayMergeStrategy getMergeStrategy(Config cfg) {
return useRecursiveMerge(cfg) ? MergeStrategy.RECURSIVE : MergeStrategy.RESOLVE;
}
@@ -143,6 +148,7 @@
private final ProjectState project;
private final boolean useContentMerge;
private final boolean useRecursiveMerge;
+ private final boolean useGitattributesForMerge;
private final PluggableCommitMessageGenerator commitMessageGenerator;
private final ChangeUtil changeUtil;
@@ -182,6 +188,7 @@
this.project = project;
this.useContentMerge = useContentMerge;
this.useRecursiveMerge = useRecursiveMerge(serverConfig);
+ this.useGitattributesForMerge = useGitattributesForMerge(serverConfig);
}
public CodeReviewCommit getFirstFastForward(
@@ -222,45 +229,16 @@
CodeReviewRevWalk rw,
int parentIndex,
boolean ignoreIdenticalTree,
- boolean allowConflicts)
- throws IOException,
- MergeIdenticalTreeException,
- MergeConflictException,
- MethodNotAllowedException,
- InvalidMergeStrategyException {
- return createCherryPickFromCommit(
- inserter,
- repoConfig,
- mergeTip,
- originalCommit,
- cherryPickCommitterIdent,
- commitMsg,
- rw,
- parentIndex,
- ignoreIdenticalTree,
- allowConflicts,
- false);
- }
-
- public CodeReviewCommit createCherryPickFromCommit(
- ObjectInserter inserter,
- Config repoConfig,
- RevCommit mergeTip,
- RevCommit originalCommit,
- PersonIdent cherryPickCommitterIdent,
- String commitMsg,
- CodeReviewRevWalk rw,
- int parentIndex,
- boolean ignoreIdenticalTree,
boolean allowConflicts,
- boolean diff3Format)
+ boolean diff3Format,
+ AttributesNodeProvider attributesNodeProvider)
throws IOException,
MergeIdenticalTreeException,
MergeConflictException,
MethodNotAllowedException,
InvalidMergeStrategyException {
- ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
+ ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig, attributesNodeProvider);
m.setBase(originalCommit.getParent(parentIndex));
DirCache dc = DirCache.newInCore();
@@ -355,24 +333,11 @@
cherryPickCommit.setMessage(commitMsg);
matchAuthorToCommitterDate(project, cherryPickCommit);
CodeReviewCommit commit = rw.parseCommit(inserter.insert(cherryPickCommit));
- commit.setFilesWithGitConflicts(filesWithGitConflicts);
+ commit.setConflicts(mergeTip, originalCommit, filesWithGitConflicts);
logger.atFine().log("CherryPick commitId=%s", commit.name());
return commit;
}
- public static ObjectId mergeWithConflicts(
- RevWalk rw,
- ObjectInserter ins,
- DirCache dc,
- String oursName,
- RevCommit ours,
- String theirsName,
- RevCommit theirs,
- Map<String, MergeResult<? extends Sequence>> mergeResults)
- throws IOException {
- return mergeWithConflicts(rw, ins, dc, oursName, ours, theirsName, theirs, mergeResults, false);
- }
-
@SuppressWarnings("resource") // TemporaryBuffer requires calling close before reading.
public static ObjectId mergeWithConflicts(
RevWalk rw,
@@ -482,37 +447,11 @@
RevCommit originalCommit,
String mergeStrategy,
boolean allowConflicts,
- PersonIdent committerIdent,
- String commitMsg,
- CodeReviewRevWalk rw)
- throws IOException,
- MergeIdenticalTreeException,
- MergeConflictException,
- InvalidMergeStrategyException {
- return createMergeCommit(
- inserter,
- repoConfig,
- mergeTip,
- originalCommit,
- mergeStrategy,
- allowConflicts,
- committerIdent,
- committerIdent,
- commitMsg,
- rw);
- }
-
- public static CodeReviewCommit createMergeCommit(
- ObjectInserter inserter,
- Config repoConfig,
- RevCommit mergeTip,
- RevCommit originalCommit,
- String mergeStrategy,
- boolean allowConflicts,
PersonIdent authorIdent,
PersonIdent committerIdent,
String commitMsg,
- CodeReviewRevWalk rw)
+ CodeReviewRevWalk rw,
+ boolean diff3Format)
throws IOException,
MergeIdenticalTreeException,
MergeConflictException,
@@ -594,7 +533,8 @@
mergeTip,
"SOURCE BRANCH",
originalCommit,
- mergeResults);
+ mergeResults,
+ diff3Format);
}
CommitBuilder mergeCommit = new CommitBuilder();
@@ -604,7 +544,7 @@
mergeCommit.setCommitter(committerIdent);
mergeCommit.setMessage(commitMsg);
CodeReviewCommit commit = rw.parseCommit(inserter.insert(mergeCommit));
- commit.setFilesWithGitConflicts(filesWithGitConflicts);
+ commit.setConflicts(mergeTip, originalCommit, filesWithGitConflicts);
return commit;
}
@@ -795,6 +735,7 @@
CodeReviewCommit mergeTip,
CodeReviewCommit toMerge) {
if (hasMissingDependencies(mergeSorter, toMerge)) {
+ logger.atFine().log("%s cannot be merged due to missing dependencies", toMerge.name());
return false;
}
@@ -803,11 +744,13 @@
private boolean canMerge(CodeReviewCommit mergeTip, Repository repo, CodeReviewCommit toMerge) {
try (ObjectInserter ins = new InMemoryInserter(repo)) {
- return newThreeWayMerger(ins, repo.getConfig()).merge(mergeTip, toMerge);
+ return newThreeWayMerger(ins, repo).merge(mergeTip, toMerge);
} catch (LargeObjectException e) {
- logger.atWarning().log("Cannot merge due to LargeObjectException: %s", toMerge.name());
+ logger.atWarning().log("%s cannot be merged due to LargeObjectException", toMerge.name());
return false;
} catch (NoMergeBaseException e) {
+ logger.atFine().log(
+ "%s cannot be merged because no merge base could be found", toMerge.name());
return false;
} catch (IOException e) {
throw new StorageException("Cannot merge " + toMerge.name(), e);
@@ -875,7 +818,7 @@
// that on the current merge tip.
//
try (ObjectInserter ins = new InMemoryInserter(repo)) {
- ThreeWayMerger m = newThreeWayMerger(ins, repo.getConfig());
+ ThreeWayMerger m = newThreeWayMerger(ins, repo);
m.setBase(toMerge.getParent(0));
return m.merge(mergeTip, toMerge);
} catch (IOException e) {
@@ -911,9 +854,10 @@
Config repoConfig,
BranchNameKey destBranch,
CodeReviewCommit mergeTip,
- CodeReviewCommit n)
+ CodeReviewCommit n,
+ AttributesNodeProvider attributesNodeProvider)
throws InvalidMergeStrategyException {
- ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig);
+ ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig, attributesNodeProvider);
try {
if (m.merge(mergeTip, n)) {
return writeMergeCommit(
@@ -1039,9 +983,20 @@
.collect(joining(",", "Merge changes ", merged.size() > 5 ? ", ..." : ""));
}
- public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Config repoConfig)
+ public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Repository repo)
throws InvalidMergeStrategyException {
- return newThreeWayMerger(inserter, repoConfig, mergeStrategyName());
+ return newThreeWayMerger(inserter, repo.getConfig(), repo.createAttributesNodeProvider());
+ }
+
+ public ThreeWayMerger newThreeWayMerger(
+ ObjectInserter inserter, Config repoConfig, AttributesNodeProvider attributesNodeProvider)
+ throws InvalidMergeStrategyException {
+ return newThreeWayMerger(
+ inserter,
+ repoConfig,
+ attributesNodeProvider,
+ mergeStrategyName(),
+ useGitattributesForMerge);
}
public String mergeStrategyName() {
@@ -1073,13 +1028,20 @@
}
public static ThreeWayMerger newThreeWayMerger(
- ObjectInserter inserter, Config repoConfig, String strategyName)
+ ObjectInserter inserter,
+ Config repoConfig,
+ AttributesNodeProvider attributesNodeProvider,
+ String strategyName,
+ boolean useGitattributesForMerge)
throws InvalidMergeStrategyException {
Merger m = newMerger(inserter, repoConfig, strategyName);
checkArgument(
m instanceof ThreeWayMerger,
"merge strategy %s does not support three-way merging",
strategyName);
+ if (m instanceof ResolveMerger && useGitattributesForMerge) {
+ ((ResolveMerger) m).setAttributesNodeProvider(attributesNodeProvider);
+ }
return (ThreeWayMerger) m;
}
diff --git a/java/com/google/gerrit/server/git/PureRevertCache.java b/java/com/google/gerrit/server/git/PureRevertCache.java
index 21da863..8a40618 100644
--- a/java/com/google/gerrit/server/git/PureRevertCache.java
+++ b/java/com/google/gerrit/server/git/PureRevertCache.java
@@ -185,7 +185,7 @@
ThreeWayMerger merger =
mergeUtilFactory
.create(projectCache.get(project).orElseThrow(illegalState(project)))
- .newThreeWayMerger(oi, repo.getConfig());
+ .newThreeWayMerger(oi, repo);
merger.setBase(claimedRevertCommit.getParent(0));
boolean success = merger.merge(claimedRevertCommit, claimedOriginalCommit);
if (!success || merger.getResultTreeId() == null) {
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index cb1af07..1d8aa05 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -36,6 +36,7 @@
import com.google.gerrit.metrics.Timer1;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PublishCommentsOp;
+import com.google.gerrit.server.RequestCounter;
import com.google.gerrit.server.cache.PerThreadCache;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.ConfigUtil;
@@ -102,7 +103,8 @@
ProjectState projectState,
IdentifiedUser user,
Repository repository,
- @Nullable MessageSender messageSender);
+ @Nullable MessageSender messageSender,
+ @Nullable RequestCounter requestCounter);
}
public static class AsyncReceiveCommitsModule extends PrivateModule {
@@ -259,7 +261,8 @@
@Assisted ProjectState projectState,
@Assisted IdentifiedUser user,
@Assisted Repository repo,
- @Assisted @Nullable MessageSender messageSender)
+ @Assisted @Nullable MessageSender messageSender,
+ @Assisted @Nullable RequestCounter requestCounter)
throws PermissionBackendException {
this.multiProgressMonitorFactory = multiProgressMonitorFactory;
this.executor = executor;
@@ -305,7 +308,8 @@
projectName,
user.getAccountId()));
receiveCommits =
- factory.create(projectState, user, receivePack, repo, allRefsWatcher, messageSender);
+ factory.create(
+ projectState, user, receivePack, repo, allRefsWatcher, messageSender, requestCounter);
receiveCommits.init();
QuotaResponse.Aggregated availableTokens =
quotaBackend.user(user).project(projectName).availableTokens(REPOSITORY_SIZE_GROUP);
diff --git a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
index 6a43719..5138db9 100644
--- a/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
+++ b/java/com/google/gerrit/server/git/receive/BranchCommitValidator.java
@@ -20,6 +20,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
@@ -28,6 +29,8 @@
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationInfo;
+import com.google.gerrit.server.git.validators.CommitValidationInfoListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.logging.TraceContext;
@@ -65,14 +68,23 @@
/** A boolean validation status and a list of additional messages. */
@AutoValue
abstract static class Result {
- static Result create(boolean isValid, ImmutableList<CommitValidationMessage> messages) {
- return new AutoValue_BranchCommitValidator_Result(isValid, messages);
+ static Result create(
+ boolean isValid,
+ ImmutableMap<String, CommitValidationInfo> validationInfos,
+ ImmutableList<CommitValidationMessage> messages) {
+ return new AutoValue_BranchCommitValidator_Result(isValid, validationInfos, messages);
}
/** Whether the commit is valid. */
abstract boolean isValid();
/**
+ * Map that maps a validator name to a {@link CommitValidationInfo} (result of running the
+ * validator).
+ */
+ abstract ImmutableMap<String, CommitValidationInfo> validationInfos();
+
+ /**
* A list of messages related to the validation. Messages may be present regardless of the
* {@link #isValid()} status.
*/
@@ -103,6 +115,8 @@
* @param cmd the ReceiveCommand executing the push.
* @param commit the commit being validated.
* @param isMerged whether this is a merge commit created by magicBranch --merge option
+ * @param invokeCommitValidationInfoListeners whether the {@link CommitValidationInfoListener}'s
+ * should be invoked when the validation is done
* @param change the change for which this is a new patchset.
* @return The validation {@link Result}.
*/
@@ -115,6 +129,7 @@
ImmutableListMultimap<String, String> pushOptions,
boolean isMerged,
NoteMap rejectCommits,
+ boolean invokeCommitValidationInfoListeners,
@Nullable Change change)
throws IOException {
return validateCommit(
@@ -126,6 +141,7 @@
pushOptions,
isMerged,
rejectCommits,
+ invokeCommitValidationInfoListeners,
change,
false);
}
@@ -138,6 +154,8 @@
* @param cmd the ReceiveCommand executing the push.
* @param commit the commit being validated.
* @param isMerged whether this is a merge commit created by magicBranch --merge option
+ * @param invokeCommitValidationInfoListeners whether the {@link CommitValidationInfoListener}'s
+ * should be invoked when the validation is done
* @param change the change for which this is a new patchset.
* @param skipValidation whether 'skip-validation' was requested.
* @return The validation {@link Result}.
@@ -151,10 +169,12 @@
ImmutableListMultimap<String, String> pushOptions,
boolean isMerged,
NoteMap rejectCommits,
+ boolean invokeCommitValidationInfoListeners,
@Nullable Change change,
boolean skipValidation)
throws IOException {
try (TraceTimer traceTimer = TraceContext.newTimer("BranchCommitValidator#validateCommit")) {
+ ImmutableMap<String, CommitValidationInfo> validationInfos = ImmutableMap.of();
ImmutableList.Builder<CommitValidationMessage> messages = new ImmutableList.Builder<>();
try (CommitReceivedEvent receiveEvent =
new CommitReceivedEvent(
@@ -185,10 +205,16 @@
skipValidation);
}
- for (CommitValidationMessage m : validators.validate(receiveEvent)) {
- messages.add(
- new CommitValidationMessage(
- messageForCommit(commit, m.getMessage(), objectReader), m.getType()));
+ validationInfos =
+ validators
+ .invokeCommitValidationInfoListeners(invokeCommitValidationInfoListeners)
+ .validate(receiveEvent);
+ for (CommitValidationInfo validatioInfo : validationInfos.values()) {
+ for (CommitValidationMessage m : validatioInfo.validationMessages()) {
+ messages.add(
+ new CommitValidationMessage(
+ messageForCommit(commit, m.getMessage(), objectReader), m.getType()));
+ }
}
} catch (CommitValidationException e) {
logger.atFine().log("Commit validation failed on %s", commit.name());
@@ -201,9 +227,9 @@
}
cmd.setResult(
REJECTED_OTHER_REASON, messageForCommit(commit, e.getMessage(), objectReader));
- return Result.create(false, messages.build());
+ return Result.create(false, ImmutableMap.of(), messages.build());
}
- return Result.create(true, messages.build());
+ return Result.create(true, validationInfos, messages.build());
}
}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 8dcfcc4..8e7b4d5 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -119,6 +119,7 @@
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.PublishCommentUtil;
import com.google.gerrit.server.PublishCommentsOp;
+import com.google.gerrit.server.RequestCounter;
import com.google.gerrit.server.RequestInfo;
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.Sequences;
@@ -152,6 +153,7 @@
import com.google.gerrit.server.git.receive.RejectionReason.MetricBucket;
import com.google.gerrit.server.git.validators.CommentCountValidator;
import com.google.gerrit.server.git.validators.CommentSizeValidator;
+import com.google.gerrit.server.git.validators.CommitValidationInfo;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.RefOperationValidationException;
import com.google.gerrit.server.git.validators.RefOperationValidators;
@@ -288,7 +290,8 @@
ReceivePack receivePack,
Repository repository,
AllRefsWatcher allRefsWatcher,
- MessageSender messageSender);
+ @Nullable MessageSender messageSender,
+ @Nullable RequestCounter requestCounter);
}
private class ReceivePackMessageSender implements MessageSender {
@@ -430,6 +433,7 @@
private final SetHashtagsOp.Factory hashtagsFactory;
private final SetTopicOp.Factory setTopicFactory;
private final ServiceUserClassifier serviceUserClassifier;
+ private final RequestCounter requestCounter;
private final ImmutableList<SubmissionListener> superprojectUpdateSubmissionListeners;
private final TagCache tagCache;
private final ProjectConfig.Factory projectConfigFactory;
@@ -453,6 +457,10 @@
// Collections populated during processing.
private final Queue<ValidationMessage> messages;
+ // Map that maps a commit SHA1 to its validation results (map that maps a validator name to its
+ // result).
+ private final Map<String, ImmutableMap<String, CommitValidationInfo>> validationInfosByCommit;
+
/** Multimap of error text to refnames that produced that error. */
private final ListMultimap<String, String> errors;
@@ -536,7 +544,8 @@
@Assisted ReceivePack rp,
@Assisted Repository repository,
@Assisted AllRefsWatcher allRefsWatcher,
- @Nullable @Assisted MessageSender messageSender)
+ @Assisted @Nullable MessageSender messageSender,
+ @Assisted @Nullable RequestCounter requestCounter)
throws IOException {
// Injected fields.
this.accountResolver = accountResolver;
@@ -580,6 +589,7 @@
this.receiveConfig = receiveConfig;
this.refValidatorsFactory = refValidatorsFactory;
this.replaceOpFactory = replaceOpFactory;
+ this.requestCounter = requestCounter;
this.requestListeners = requestListeners;
this.retryHelper = retryHelper;
this.requestScopePropagator = requestScopePropagator;
@@ -606,6 +616,8 @@
rejectCommits = BanCommit.loadRejectCommitsMap(repo, rp.getRevWalk());
// Collections populated during processing.
+ validationInfosByCommit = new LinkedHashMap<>();
+
errors = MultimapBuilder.linkedHashKeys().arrayListValues().build();
rejectionReasons = new LinkedHashMap<>();
messages = new ConcurrentLinkedQueue<>();
@@ -706,7 +718,8 @@
TraceTimer traceTimer =
newTimer("processCommands", Metadata.builder().resourceCount(commandCount))) {
RequestInfo requestInfo =
- RequestInfo.builder(RequestInfo.RequestType.GIT_RECEIVE, user, traceContext)
+ RequestInfo.builder(
+ RequestInfo.RequestType.GIT_RECEIVE, "git-receive-pack", user, traceContext)
.project(project.getNameKey())
.build();
requestListeners.runEach(l -> l.onRequest(requestInfo));
@@ -722,6 +735,7 @@
commands =
commands.stream().map(c -> wrapReceiveCommand(c, commandProgress)).collect(toList());
+ Throwable error = null;
try (RequestStateContext requestStateContext =
RequestStateContext.open()
.addRequestStateProvider(progress)
@@ -732,9 +746,11 @@
commands,
RejectionReason.create(MetricBucket.INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR));
} catch (InvalidDeadlineException e) {
+ error = e;
rejectRemaining(
commands, RejectionReason.create(MetricBucket.INVALID_DEADLINE, e.getMessage()));
} catch (RuntimeException e) {
+ error = e;
Optional<RequestCancelledException> requestCancelledException =
RequestCancelledException.getFromCausalChain(e);
if (!requestCancelledException.isPresent()) {
@@ -764,6 +780,10 @@
}
rejectRemaining(commands, RejectionReason.create(metricBucket, msg.toString()));
+ } finally {
+ if (requestCounter != null) {
+ requestCounter.countRequest(requestInfo, error);
+ }
}
// This sends error messages before the 'done' string of the progress monitor is sent.
@@ -2657,6 +2677,9 @@
"Creating new change for %s even though it is already tracked", name);
}
+ // Validate the received commits. Do not invoke the CommitValidationInfoListener's yet
+ // because we create changes/patch-sets for the commits only later and we need to provide
+ // the patch set ID, that we don't know yet, to CommitValidationInfoListener's.
BranchCommitValidator.Result validationResult =
validator.validateCommit(
repo,
@@ -2668,7 +2691,9 @@
ImmutableListMultimap.copyOf(pushOptions),
magicBranch.merged,
rejectCommits,
- null);
+ /* invokeCommitValidationInfoListeners= */ false,
+ /* change= */ null);
+ validationInfosByCommit.put(c.name(), validationResult.validationInfos());
messages.addAll(validationResult.messages());
if (!validationResult.isValid()) {
// Not a change the user can propose? Abort as early as possible.
@@ -3021,8 +3046,10 @@
.setTopic(magicBranch.topic)
.setPrivate(setChangeAsPrivate)
.setWorkInProgress(magicBranch.shouldSetWorkInProgressOnNewChanges())
- // Changes already validated in validateNewCommits.
- .setValidate(false);
+ // The commit has already been validated in
+ // selectNewAndReplacedChangesFromMagicBranch.
+ .setValidationOptions(ImmutableListMultimap.copyOf(pushOptions))
+ .disableValidation(validationInfosByCommit.get(commit.name()));
if (magicBranch.merged) {
ins.setStatus(Change.Status.MERGED);
@@ -3537,6 +3564,10 @@
priorCommit,
psId,
newCommit,
+ // The commit has already been validated in
+ // selectNewAndReplacedChangesFromMagicBranch.
+ ImmutableListMultimap.copyOf(pushOptions),
+ validationInfosByCommit.get(newCommit.name()),
info,
groups,
magicBranch,
@@ -3751,9 +3782,10 @@
cmd,
c,
ImmutableListMultimap.copyOf(pushOptions),
- false,
+ /* isMerged= */ false,
rejectCommits,
- null,
+ /* invokeCommitValidationInfoListeners= */ true,
+ /* change= */ null,
skipValidation);
messages.addAll(validationResult.messages());
if (!validationResult.isValid()) {
diff --git a/java/com/google/gerrit/server/git/receive/ReplaceOp.java b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
index 60e1a09..87ed6e7 100644
--- a/java/com/google/gerrit/server/git/receive/ReplaceOp.java
+++ b/java/com/google/gerrit/server/git/receive/ReplaceOp.java
@@ -26,6 +26,8 @@
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
@@ -60,16 +62,21 @@
import com.google.gerrit.server.change.ReviewerModifier.ReviewerModificationList;
import com.google.gerrit.server.change.ReviewerOp;
import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.extensions.events.CommentAdded;
import com.google.gerrit.server.extensions.events.RevisionCreated;
import com.google.gerrit.server.git.MergedByPushOp;
import com.google.gerrit.server.git.receive.ReceiveCommits.MagicBranchInput;
import com.google.gerrit.server.git.receive.RejectionReason.MetricBucket;
+import com.google.gerrit.server.git.validators.CommitValidationInfo;
+import com.google.gerrit.server.git.validators.CommitValidationInfoListener;
import com.google.gerrit.server.git.validators.TopicValidator;
import com.google.gerrit.server.mail.MailUtil.MailRecipients;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.patch.DiffOperationsForCommitValidation;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
@@ -111,6 +118,8 @@
@Assisted("priorCommitId") ObjectId priorCommit,
@Assisted("patchSetId") PatchSet.Id patchSetId,
@Assisted("commitId") ObjectId commitId,
+ ImmutableListMultimap<String, String> pushOptions,
+ ImmutableMap<String, CommitValidationInfo> validationInfos,
PatchSetInfo info,
List<String> groups,
@Nullable MagicBranchInput magicBranch,
@@ -136,6 +145,8 @@
private final ReviewerModifier reviewerModifier;
private final ChangeUtil changeUtil;
private final TopicValidator topicValidator;
+ private final DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory;
+ private final PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners;
private final ProjectState projectState;
private final Change change;
@@ -145,6 +156,8 @@
private final ObjectId priorCommitId;
private final PatchSet.Id patchSetId;
private final ObjectId commitId;
+ private final ImmutableListMultimap<String, String> pushOptions;
+ private final ImmutableMap<String, CommitValidationInfo> validationInfos;
private final PatchSetInfo info;
private final MagicBranchInput magicBranch;
private final PushCertificate pushCertificate;
@@ -182,6 +195,8 @@
ReviewerModifier reviewerModifier,
ChangeUtil changeUtil,
TopicValidator topicValidator,
+ DiffOperationsForCommitValidation.Factory diffOperationsForCommitValidationFactory,
+ PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners,
@Assisted ProjectState projectState,
@Assisted Change change,
@Assisted boolean checkMergedInto,
@@ -190,6 +205,8 @@
@Assisted("priorCommitId") ObjectId priorCommitId,
@Assisted("patchSetId") PatchSet.Id patchSetId,
@Assisted("commitId") ObjectId commitId,
+ @Assisted ImmutableListMultimap<String, String> pushOptions,
+ @Assisted @Nullable ImmutableMap<String, CommitValidationInfo> validationInfos,
@Assisted PatchSetInfo info,
@Assisted List<String> groups,
@Assisted @Nullable MagicBranchInput magicBranch,
@@ -211,6 +228,8 @@
this.reviewerModifier = reviewerModifier;
this.changeUtil = changeUtil;
this.topicValidator = topicValidator;
+ this.diffOperationsForCommitValidationFactory = diffOperationsForCommitValidationFactory;
+ this.commitValidationInfoListeners = commitValidationInfoListeners;
this.projectState = projectState;
this.change = change;
@@ -220,6 +239,8 @@
this.priorCommitId = priorCommitId.copy();
this.patchSetId = patchSetId;
this.commitId = commitId.copy();
+ this.pushOptions = pushOptions;
+ this.validationInfos = validationInfos;
this.info = info;
this.groups = groups;
this.magicBranch = magicBranch;
@@ -236,6 +257,7 @@
projectState.getNameKey(),
ctx.getRevWalk(),
ctx.getRepoView().getConfig(),
+ ctx.getRepoView().getAttributesNodeProvider(),
priorCommitId,
commitId);
@@ -254,6 +276,25 @@
cmd = new ReceiveCommand(ObjectId.zeroId(), commitId, patchSetId.toRefName());
ctx.addRefUpdate(cmd);
+
+ if (validationInfos != null) {
+ try (CommitReceivedEvent event =
+ new CommitReceivedEvent(
+ cmd,
+ projectState.getProject(),
+ change.getDest().branch(),
+ pushOptions,
+ ctx.getRepoView().getConfig(),
+ ctx.getRevWalk().getObjectReader(),
+ commitId,
+ ctx.getIdentifiedUser(),
+ diffOperationsForCommitValidationFactory.create(
+ ctx.getRepoView(), ctx.getInserter()))) {
+ commitValidationInfoListeners.runEach(
+ commitValidationInfoListener ->
+ commitValidationInfoListener.commitValidated(validationInfos, event, patchSetId));
+ }
+ }
}
@Override
@@ -432,7 +473,7 @@
message.append("\n\n").append(reviewMessage);
}
approvalsUtil
- .formatApprovalCopierResult(approvalCopierResult, projectState.getLabelTypes())
+ .formatApprovalCopierResult(approvalCopierResult)
.ifPresent(
msg -> {
if (Strings.isNullOrEmpty(reviewMessage) || !reviewMessage.endsWith("\n")) {
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidationInfo.java b/java/com/google/gerrit/server/git/validators/CommitValidationInfo.java
new file mode 100644
index 0000000..85bbded
--- /dev/null
+++ b/java/com/google/gerrit/server/git/validators/CommitValidationInfo.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2024 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.validators;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Result of invoking a {@link CommitValidationListener} if the commit passed the validator.
+ *
+ * <p>Note, if a commit is rejected by a {@link CommitValidationListener} it throws a {@link
+ * CommitValidationException} and no {@code CommitValidationInfo} is returned. Hence {@code
+ * CommitValidationInfo} doesn't cover rejections.
+ */
+@AutoValue
+public abstract class CommitValidationInfo {
+ /** Empty metadata map. */
+ public static final ImmutableMap<String, String> NO_METADATA = ImmutableMap.of();
+
+ public enum Status {
+ /** The validation has been performed and the commit passed the validation. */
+ PASSED,
+
+ /**
+ * The validation was not done because it was not applicable, for example the validator
+ * configuration didn't match the commit that was uploaded/created.
+ */
+ NOT_APPLICABLE,
+
+ /** The validation has been skipped by the user. */
+ SKIPPED_BY_USER,
+ }
+
+ /** Status of the commit validation run. */
+ public abstract Status status();
+
+ /**
+ * Metadata about the commit validation that has been performed, for example the version ID of the
+ * configuration that was used for the commit validation or the SHA1 from which the configuration
+ * that was used for the commit validation was read.
+ */
+ public abstract ImmutableMap<String, String> metadata();
+
+ /** Validation messages collected during the commit validation run. */
+ public abstract ImmutableList<CommitValidationMessage> validationMessages();
+
+ public static CommitValidationInfo passed(
+ ImmutableMap<String, String> metadata,
+ ImmutableList<CommitValidationMessage> validationMessages) {
+ return new AutoValue_CommitValidationInfo(Status.PASSED, metadata, validationMessages);
+ }
+
+ public static CommitValidationInfo notApplicable(ImmutableMap<String, String> metadata) {
+ return new AutoValue_CommitValidationInfo(Status.NOT_APPLICABLE, metadata, ImmutableList.of());
+ }
+
+ public static CommitValidationInfo skippedByUser(ImmutableMap<String, String> metadata) {
+ return new AutoValue_CommitValidationInfo(Status.SKIPPED_BY_USER, metadata, ImmutableList.of());
+ }
+}
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidationInfoListener.java b/java/com/google/gerrit/server/git/validators/CommitValidationInfoListener.java
new file mode 100644
index 0000000..27c0059
--- /dev/null
+++ b/java/com/google/gerrit/server/git/validators/CommitValidationInfoListener.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2024 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.validators;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+
+/**
+ * Extension point that is invoked after a commit has passed the validations that are done by {@code
+ * CommitValidationListener}'s.
+ *
+ * <p>If any {@code CommitValidationListener} rejects the commit (by throwing a {@code
+ * CommitValidationException}) this extension point is not invoked.
+ */
+@ExtensionPoint
+public interface CommitValidationInfoListener {
+ /**
+ * Invoked after a commit has passed the validation that is done by {@code
+ * CommitValidationListener}'s
+ *
+ * <p>Not invoked if any {@code CommitValidationListener} rejects the commit (by throwing a {@code
+ * CommitValidationException}).
+ *
+ * @param validationInfoByValidator Map that maps a validator name to a {@link
+ * CommitValidationInfo} (result of running the validator).
+ * @param receiveEvent The receive event for which the validation was done. Contains data about
+ * which commit was validated and what is the updated ref.
+ * @param patchSetId if the validation was done for a patch set, the ID of the patch set,
+ * otherwise {@code null}
+ */
+ void commitValidated(
+ ImmutableMap<String, CommitValidationInfo> validationInfoByValidator,
+ CommitReceivedEvent receiveEvent,
+ @Nullable PatchSet.Id patchSetId);
+}
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidationListener.java b/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
index 9f68c0d..d1c8e3d 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
@@ -14,6 +14,9 @@
package com.google.gerrit.server.git.validators;
+import static com.google.gerrit.server.git.validators.CommitValidationInfo.NO_METADATA;
+
+import com.google.common.collect.ImmutableList;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import com.google.gerrit.server.events.CommitReceivedEvent;
import java.util.List;
@@ -32,14 +35,45 @@
@ExtensionPoint
public interface CommitValidationListener {
/**
- * Commit validation.
+ * Name of the validator.
+ *
+ * <p>Must return a unique name (i.e. a name that is not used by any other validator).
+ */
+ default String getValidatorName() {
+ return getClass().getName();
+ }
+
+ /**
+ * Runs a commit validation.
+ *
+ * <p>This method only exist for backwards-compatibility and doesn't need to be implemented when
+ * {@link #validateCommit(CommitReceivedEvent)} is implemented.
*
* @param receiveEvent commit event details
- * @return list of validation messages
- * @throws CommitValidationException if validation fails
+ * @return list of validation messages if the commit passes the validation
+ * @throws CommitValidationException if validation fails and the commit is rejected
+ * @deprecated use {@link #validateCommit(CommitReceivedEvent)} instead
*/
- List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
- throws CommitValidationException;
+ @Deprecated
+ default List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ throw new IllegalStateException("not implemented");
+ }
+
+ /**
+ * Runs a commit validation.
+ *
+ * <p>Implement this method instead of {@link #onCommitReceived(CommitReceivedEvent)}.
+ *
+ * @param receiveEvent commit event details
+ * @return result of the commit validation if the commit passes the validation
+ * @throws CommitValidationException if validation fails and the commit is rejected
+ */
+ default CommitValidationInfo validateCommit(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ return CommitValidationInfo.passed(
+ NO_METADATA, ImmutableList.copyOf(onCommitReceived(receiveEvent)));
+ }
/**
* Whether this validator should validate all commits.
diff --git a/java/com/google/gerrit/server/git/validators/CommitValidators.java b/java/com/google/gerrit/server/git/validators/CommitValidators.java
index 2311240..9696e12 100644
--- a/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -15,26 +15,28 @@
package com.google.gerrit.server.git.validators;
import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.gerrit.entities.Change.CHANGE_ID_PATTERN;
import static com.google.gerrit.entities.RefNames.REFS_CHANGES;
import static com.google.gerrit.entities.RefNames.REFS_CONFIG;
+import static com.google.gerrit.server.git.validators.CommitValidationInfo.NO_METADATA;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
-import static java.util.stream.Collectors.toList;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.gerrit.common.FooterConstants;
import com.google.gerrit.common.Nullable;
-import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Patch;
+import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.metrics.Counter2;
@@ -44,14 +46,11 @@
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.UrlFormatter;
import com.google.gerrit.server.events.CommitReceivedEvent;
-import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.ValidationError;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
@@ -62,6 +61,7 @@
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.gerrit.server.plugincontext.PluginSetEntryContext;
import com.google.gerrit.server.project.LabelConfigValidator;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
@@ -77,6 +77,7 @@
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -85,7 +86,6 @@
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.FooterKey;
import org.eclipse.jgit.revwalk.FooterLine;
@@ -108,18 +108,15 @@
private final PersonIdent gerritIdent;
private final DynamicItem<UrlFormatter> urlFormatter;
private final PluginSetContext<CommitValidationListener> pluginValidators;
- private final GitRepositoryManager repoManager;
private final AllUsersName allUsers;
private final AllProjectsName allProjects;
- private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
- private final AccountValidator accountValidator;
- private final AccountCache accountCache;
private final ProjectCache projectCache;
private final ProjectConfig.Factory projectConfigFactory;
private final Config config;
private final ChangeUtil changeUtil;
private final MetricMaker metricMaker;
private final ApprovalQueryBuilder approvalQueryBuilder;
+ private final PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners;
@Inject
Factory(
@@ -127,32 +124,26 @@
DynamicItem<UrlFormatter> urlFormatter,
@GerritServerConfig Config config,
PluginSetContext<CommitValidationListener> pluginValidators,
- GitRepositoryManager repoManager,
AllUsersName allUsers,
AllProjectsName allProjects,
- ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
- AccountValidator accountValidator,
- AccountCache accountCache,
ProjectCache projectCache,
ProjectConfig.Factory projectConfigFactory,
ChangeUtil changeUtil,
MetricMaker metricMaker,
- ApprovalQueryBuilder approvalQueryBuilder) {
+ ApprovalQueryBuilder approvalQueryBuilder,
+ PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners) {
this.gerritIdent = gerritIdent;
this.urlFormatter = urlFormatter;
this.config = config;
this.pluginValidators = pluginValidators;
- this.repoManager = repoManager;
this.allUsers = allUsers;
this.allProjects = allProjects;
- this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
- this.accountValidator = accountValidator;
- this.accountCache = accountCache;
this.projectCache = projectCache;
this.projectConfigFactory = projectConfigFactory;
this.changeUtil = changeUtil;
this.metricMaker = metricMaker;
this.approvalQueryBuilder = approvalQueryBuilder;
+ this.commitValidationInfoListeners = commitValidationInfoListeners;
}
public CommitValidators forReceiveCommits(
@@ -180,13 +171,19 @@
new ChangeIdValidator(
changeUtil, projectState, user, urlFormatter.get(), config, sshInfo, change))
.add(new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects))
- .add(new BannedCommitsValidator(rejectCommits))
- .add(new PluginCommitValidationListener(pluginValidators, skipValidation))
- .add(new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker, accountCache))
- .add(new AccountCommitValidator(repoManager, allUsers, accountValidator))
+ .add(new BannedCommitsValidator(rejectCommits));
+
+ Iterator<PluginSetEntryContext<CommitValidationListener>> pluginValidatorsIt =
+ pluginValidators.iterator();
+ while (pluginValidatorsIt.hasNext()) {
+ validators.add(skippablePluginValidator(pluginValidatorsIt.next().get(), skipValidation));
+ }
+
+ validators
.add(new GroupCommitValidator(allUsers))
.add(new LabelConfigValidator(approvalQueryBuilder));
- return new CommitValidators(validators.build());
+
+ return new CommitValidators(commitValidationInfoListeners, validators.build());
}
public CommitValidators forGerritCommits(
@@ -210,13 +207,19 @@
.add(
new ChangeIdValidator(
changeUtil, projectState, user, urlFormatter.get(), config, sshInfo, change))
- .add(new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects))
- .add(new PluginCommitValidationListener(pluginValidators))
- .add(new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker, accountCache))
- .add(new AccountCommitValidator(repoManager, allUsers, accountValidator))
+ .add(new ConfigValidator(projectConfigFactory, branch, user, rw, allUsers, allProjects));
+
+ Iterator<PluginSetEntryContext<CommitValidationListener>> pluginValidatorsIt =
+ pluginValidators.iterator();
+ while (pluginValidatorsIt.hasNext()) {
+ validators.add(pluginValidatorsIt.next().get());
+ }
+
+ validators
.add(new GroupCommitValidator(allUsers))
.add(new LabelConfigValidator(approvalQueryBuilder));
- return new CommitValidators(validators.build());
+
+ return new CommitValidators(commitValidationInfoListeners, validators.build());
}
public CommitValidators forMergedCommits(
@@ -243,22 +246,84 @@
.add(new ProjectStateValidationListener(projectState))
.add(new AuthorUploaderValidator(user, perm, urlFormatter.get()))
.add(new CommitterUploaderValidator(user, perm, urlFormatter.get()));
- return new CommitValidators(validators.build());
+ return new CommitValidators(commitValidationInfoListeners, validators.build());
+ }
+
+ CommitValidationListener skippablePluginValidator(
+ CommitValidationListener pluginValidator, boolean skipValidation) {
+ return new CommitValidationListener() {
+ @Override
+ public String getValidatorName() {
+ return pluginValidator.getValidatorName();
+ }
+
+ @Override
+ public CommitValidationInfo validateCommit(CommitReceivedEvent receiveEvent)
+ throws CommitValidationException {
+ if (skipValidation && !pluginValidator.shouldValidateAllCommits()) {
+ return CommitValidationInfo.skippedByUser(NO_METADATA);
+ }
+ return pluginValidator.validateCommit(receiveEvent);
+ }
+ };
}
}
+ private final PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners;
private final List<CommitValidationListener> validators;
- CommitValidators(List<CommitValidationListener> validators) {
+ @Nullable private PatchSet.Id patchSetId;
+ private boolean invokeCommitValidationInfoListeners = true;
+
+ CommitValidators(
+ PluginSetContext<CommitValidationInfoListener> commitValidationInfoListeners,
+ List<CommitValidationListener> validators) {
+ this.commitValidationInfoListeners = commitValidationInfoListeners;
this.validators = validators;
}
+ /**
+ * Sets the patch set for which the validation is done.
+ *
+ * <p>If the validation is done for a commit that is not associated with a patch set (e.g. when
+ * commits are pushed directly, bypassing code-review) this method doesn't need to be called.
+ *
+ * @param patchSetId the patch set for which the validation is done
+ * @return the {@link CommitValidators} instance to allow chaining calls
+ */
@CanIgnoreReturnValue
- public List<CommitValidationMessage> validate(CommitReceivedEvent receiveEvent)
+ public CommitValidators patchSet(PatchSet.Id patchSetId) {
+ this.patchSetId = patchSetId;
+ return this;
+ }
+
+ /**
+ * Whether the {@link CommitValidationInfoListener}s should be invoked after the validation is
+ * done.
+ *
+ * <p>If invoking the {@link CommitValidationInfoListener}s is skipped, it's the responsibility of
+ * the caller to invoke them later. For example, this makes sense when commits are validated and
+ * the information about the change/patch-set (see {@link
+ * #patchSet(com.google.gerrit.entities.PatchSet.Id)} is not available yet.
+ *
+ * @param invokeCommitValidationInfoListeners Whether the {@link CommitValidationInfoListener}s
+ * should be invoked after the validation is done.
+ * @return the {@link CommitValidators} instance to allow chaining calls
+ */
+ @CanIgnoreReturnValue
+ public CommitValidators invokeCommitValidationInfoListeners(
+ boolean invokeCommitValidationInfoListeners) {
+ this.invokeCommitValidationInfoListeners = invokeCommitValidationInfoListeners;
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ public ImmutableMap<String, CommitValidationInfo> validate(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
- List<CommitValidationMessage> messages = new ArrayList<>();
- try {
- for (CommitValidationListener commitValidator : validators) {
+ ImmutableMap.Builder<String, CommitValidationInfo> validationInfosBuilder =
+ ImmutableMap.builder();
+ for (CommitValidationListener commitValidator : validators) {
+ try {
try (TraceTimer ignored =
TraceContext.newTimer(
"Running CommitValidationListener",
@@ -268,17 +333,37 @@
.branchName(receiveEvent.getBranchNameKey().branch())
.commit(receiveEvent.commit.name())
.build())) {
- messages.addAll(commitValidator.onCommitReceived(receiveEvent));
+ CommitValidationInfo commitValidationInfo = commitValidator.validateCommit(receiveEvent);
+ logger.atFine().log(
+ "commit %s has passed validator %s: %s",
+ receiveEvent.commit.name(), commitValidator.getValidatorName(), commitValidationInfo);
+ validationInfosBuilder.put(commitValidator.getValidatorName(), commitValidationInfo);
}
+ } catch (CommitValidationException e) {
+ // Keep the old messages (and their order) in case of an exception
+ ImmutableList<CommitValidationMessage> messages =
+ Streams.concat(
+ validationInfosBuilder.build().values().stream()
+ .flatMap(validationInfo -> validationInfo.validationMessages().stream()),
+ e.getMessages().stream())
+ .collect(toImmutableList());
+ logger.atFine().withCause(e).log(
+ "commit %s was rejected by validator %s: %s",
+ receiveEvent.commit.name(), commitValidator.getValidatorName(), messages);
+ throw new CommitValidationException(e.getMessage(), messages);
}
- } catch (CommitValidationException e) {
- logger.atFine().withCause(e).log(
- "CommitValidationException occurred: %s", e.getFullMessage());
- // Keep the old messages (and their order) in case of an exception
- messages.addAll(e.getMessages());
- throw new CommitValidationException(e.getMessage(), messages);
}
- return messages;
+
+ ImmutableMap<String, CommitValidationInfo> validationInfos = validationInfosBuilder.build();
+
+ if (invokeCommitValidationInfoListeners) {
+ commitValidationInfoListeners.runEach(
+ commitValidationInfoListener ->
+ commitValidationInfoListener.commitValidated(
+ validationInfos, receiveEvent, patchSetId));
+ }
+
+ return validationInfos;
}
public static class ChangeIdValidator implements CommitValidationListener {
@@ -641,55 +726,6 @@
}
}
- /** Execute commit validation plug-ins */
- public static class PluginCommitValidationListener implements CommitValidationListener {
- private final boolean skipValidation;
- private final PluginSetContext<CommitValidationListener> commitValidationListeners;
-
- public PluginCommitValidationListener(
- final PluginSetContext<CommitValidationListener> commitValidationListeners) {
- this(commitValidationListeners, false);
- }
-
- public PluginCommitValidationListener(
- final PluginSetContext<CommitValidationListener> commitValidationListeners,
- boolean skipValidation) {
- this.skipValidation = skipValidation;
- this.commitValidationListeners = commitValidationListeners;
- }
-
- private void runValidator(
- CommitValidationListener validator,
- List<CommitValidationMessage> messages,
- CommitReceivedEvent receiveEvent)
- throws CommitValidationException {
- if (skipValidation && !validator.shouldValidateAllCommits()) {
- return;
- }
- messages.addAll(validator.onCommitReceived(receiveEvent));
- }
-
- @Override
- public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
- throws CommitValidationException {
- List<CommitValidationMessage> messages = new ArrayList<>();
- try {
- commitValidationListeners.runEach(
- l -> runValidator(l, messages, receiveEvent), CommitValidationException.class);
- } catch (CommitValidationException e) {
- messages.addAll(e.getMessages());
- throw new CommitValidationException(e.getMessage(), messages);
- }
- return messages;
- }
-
- @Override
- public boolean shouldValidateAllCommits() {
- return commitValidationListeners.stream()
- .anyMatch(CommitValidationListener::shouldValidateAllCommits);
- }
- }
-
public static class SignedOffByValidator implements CommitValidationListener {
private final IdentifiedUser user;
private final PermissionBackend.ForRef perm;
@@ -872,106 +908,6 @@
}
}
- /** Validates updates to refs/meta/external-ids. */
- public static class ExternalIdUpdateListener implements CommitValidationListener {
- private final AllUsersName allUsers;
- private final AccountCache accountCache;
- private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
-
- public ExternalIdUpdateListener(
- AllUsersName allUsers,
- ExternalIdsConsistencyChecker externalIdsConsistencyChecker,
- AccountCache accountCache) {
- this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
- this.allUsers = allUsers;
- this.accountCache = accountCache;
- }
-
- @Override
- public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
- throws CommitValidationException {
- if (allUsers.equals(receiveEvent.project.getNameKey())
- && RefNames.REFS_EXTERNAL_IDS.equals(receiveEvent.refName)) {
- try {
- List<ConsistencyProblemInfo> problems =
- externalIdsConsistencyChecker.check(accountCache, receiveEvent.commit);
- List<CommitValidationMessage> msgs =
- problems.stream()
- .map(
- p ->
- new CommitValidationMessage(
- p.message,
- p.status == ConsistencyProblemInfo.Status.ERROR
- ? ValidationMessage.Type.ERROR
- : ValidationMessage.Type.OTHER))
- .collect(toList());
- if (msgs.stream().anyMatch(ValidationMessage::isError)) {
- throw new CommitValidationException("invalid external IDs", msgs);
- }
- return msgs;
- } catch (IOException | ConfigInvalidException e) {
- throw new CommitValidationException("error validating external IDs", e);
- }
- }
- return Collections.emptyList();
- }
- }
-
- public static class AccountCommitValidator implements CommitValidationListener {
- private final GitRepositoryManager repoManager;
- private final AllUsersName allUsers;
- private final AccountValidator accountValidator;
-
- public AccountCommitValidator(
- GitRepositoryManager repoManager,
- AllUsersName allUsers,
- AccountValidator accountValidator) {
- this.repoManager = repoManager;
- this.allUsers = allUsers;
- this.accountValidator = accountValidator;
- }
-
- @Override
- public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
- throws CommitValidationException {
- if (!allUsers.equals(receiveEvent.project.getNameKey())) {
- return Collections.emptyList();
- }
-
- if (receiveEvent.command.getRefName().startsWith(MagicBranch.NEW_CHANGE)) {
- // no validation on push for review, will be checked on submit by
- // MergeValidators.AccountMergeValidator
- return Collections.emptyList();
- }
-
- Account.Id accountId = Account.Id.fromRef(receiveEvent.refName);
- if (accountId == null) {
- return Collections.emptyList();
- }
-
- try (Repository repo = repoManager.openRepository(allUsers)) {
- List<String> errorMessages =
- accountValidator.validate(
- accountId,
- repo,
- receiveEvent.revWalk,
- receiveEvent.command.getOldId(),
- receiveEvent.commit);
- if (!errorMessages.isEmpty()) {
- throw new CommitValidationException(
- "invalid account configuration",
- errorMessages.stream()
- .map(m -> new CommitValidationMessage(m, ValidationMessage.Type.ERROR))
- .collect(toList()));
- }
- } catch (IOException e) {
- throw new CommitValidationException(
- String.format("Validating update for account %s failed", accountId.get()), e);
- }
- return Collections.emptyList();
- }
- }
-
/** Rejects updates to group branches. */
public static class GroupCommitValidator implements CommitValidationListener {
private final AllUsersName allUsers;
diff --git a/java/com/google/gerrit/server/git/validators/MergeValidators.java b/java/com/google/gerrit/server/git/validators/MergeValidators.java
index c8a3d1e..654b5d0 100644
--- a/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -14,10 +14,8 @@
package com.google.gerrit.server.git.validators;
-import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
@@ -28,7 +26,6 @@
import com.google.gerrit.extensions.registration.Extension;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountProperties;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.GerritServerConfig;
@@ -48,7 +45,6 @@
import com.google.gerrit.server.query.change.ChangeData;
import com.google.inject.Inject;
import java.io.IOException;
-import java.util.List;
import java.util.Objects;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
@@ -69,7 +65,6 @@
private final PluginSetContext<MergeValidationListener> mergeValidationListeners;
private final ProjectConfigValidator.Factory projectConfigValidatorFactory;
- private final AccountMergeValidator.Factory accountValidatorFactory;
private final GroupMergeValidator.Factory groupValidatorFactory;
public interface Factory {
@@ -80,11 +75,9 @@
MergeValidators(
PluginSetContext<MergeValidationListener> mergeValidationListeners,
ProjectConfigValidator.Factory projectConfigValidatorFactory,
- AccountMergeValidator.Factory accountValidatorFactory,
GroupMergeValidator.Factory groupValidatorFactory) {
this.mergeValidationListeners = mergeValidationListeners;
this.projectConfigValidatorFactory = projectConfigValidatorFactory;
- this.accountValidatorFactory = accountValidatorFactory;
this.groupValidatorFactory = groupValidatorFactory;
}
@@ -105,7 +98,6 @@
ImmutableList.of(
new PluginMergeValidationListener(mergeValidationListeners),
projectConfigValidatorFactory.create(),
- accountValidatorFactory.create(),
groupValidatorFactory.create(),
new DestBranchRefValidator());
@@ -280,65 +272,6 @@
}
}
- public static class AccountMergeValidator implements MergeValidationListener {
- public interface Factory {
- AccountMergeValidator create();
- }
-
- private final AllUsersName allUsersName;
- private final ChangeData.Factory changeDataFactory;
- private final AccountValidator accountValidator;
-
- @Inject
- public AccountMergeValidator(
- AllUsersName allUsersName,
- ChangeData.Factory changeDataFactory,
- AccountValidator accountValidator) {
- this.allUsersName = allUsersName;
- this.changeDataFactory = changeDataFactory;
- this.accountValidator = accountValidator;
- }
-
- @Override
- public void onPreMerge(
- Repository repo,
- CodeReviewRevWalk revWalk,
- CodeReviewCommit commit,
- ProjectState destProject,
- BranchNameKey destBranch,
- PatchSet.Id patchSetId,
- IdentifiedUser caller)
- throws MergeValidationException {
- Account.Id accountId = Account.Id.fromRef(destBranch.branch());
- if (!allUsersName.equals(destProject.getNameKey()) || accountId == null) {
- return;
- }
-
- ChangeData cd =
- changeDataFactory.create(destProject.getProject().getNameKey(), patchSetId.changeId());
- try {
- if (!cd.currentFilePaths().contains(AccountProperties.ACCOUNT_CONFIG)) {
- return;
- }
- } catch (StorageException e) {
- logger.atSevere().withCause(e).log("Cannot validate account update");
- throw new MergeValidationException("account validation unavailable", e);
- }
-
- try {
- List<String> errorMessages =
- accountValidator.validate(accountId, repo, revWalk, null, commit);
- if (!errorMessages.isEmpty()) {
- throw new MergeValidationException(
- "invalid account configuration: " + Joiner.on("; ").join(errorMessages));
- }
- } catch (IOException e) {
- logger.atSevere().withCause(e).log("Cannot validate account update");
- throw new MergeValidationException("account validation unavailable", e);
- }
- }
- }
-
/** Validator to ensure that group refs are not mutated. */
public static class GroupMergeValidator implements MergeValidationListener {
public interface Factory {
diff --git a/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
index d466041..e6bd019 100644
--- a/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
+++ b/java/com/google/gerrit/server/group/PeriodicGroupIndexer.java
@@ -15,18 +15,38 @@
package com.google.gerrit.server.group;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.flogger.FluentLogger;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.InternalGroup;
+import com.google.gerrit.index.IndexConfig;
+import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.query.DataSource;
+import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.group.db.GroupNameNotes;
+import com.google.gerrit.server.index.IndexExecutor;
+import com.google.gerrit.server.index.group.GroupField;
+import com.google.gerrit.server.index.group.GroupIndexCollection;
import com.google.gerrit.server.index.group.GroupIndexer;
import com.google.inject.Inject;
import com.google.inject.Provider;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
import org.eclipse.jgit.lib.Repository;
/**
@@ -41,58 +61,87 @@
* information until the next indexing happens. The interval on which group indexing is done is
* configurable by setting {@code index.scheduledIndexer.interval} in {@code gerrit.config}. By
* default group indexing is done every 5 minutes.
- *
- * <p>This class is not able to detect group deletions that were replicated while the slave was
- * offline. This means if group refs are deleted while the slave is offline these groups are not
- * removed from the group index when the slave is started. However since group deletion is not
- * supported this should never happen and one can always do an offline reindex before starting the
- * slave.
*/
public class PeriodicGroupIndexer implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final AllUsersName allUsersName;
private final GitRepositoryManager repoManager;
+ private final ListeningExecutorService executor;
+ private final GroupIndexCollection indexes;
private final Provider<GroupIndexer> groupIndexerProvider;
-
- private ImmutableSet<AccountGroup.UUID> groupUuids;
+ private final IndexConfig indexConfig;
@Inject
PeriodicGroupIndexer(
AllUsersName allUsersName,
GitRepositoryManager repoManager,
+ @IndexExecutor(BATCH) ListeningExecutorService executor,
+ GroupIndexCollection indexes,
+ IndexConfig indexConfig,
Provider<GroupIndexer> groupIndexerProvider) {
this.allUsersName = allUsersName;
this.repoManager = repoManager;
+ this.executor = executor;
+ this.indexes = indexes;
+ this.indexConfig = indexConfig;
this.groupIndexerProvider = groupIndexerProvider;
}
@Override
public synchronized void run() {
try (Repository allUsers = repoManager.openRepository(allUsersName)) {
- ImmutableSet<AccountGroup.UUID> newGroupUuids =
+ ImmutableSet<AccountGroup.UUID> allGroups =
GroupNameNotes.loadAllGroups(allUsers).stream()
.map(GroupReference::getUUID)
.collect(toImmutableSet());
GroupIndexer groupIndexer = groupIndexerProvider.get();
- int reindexCounter = 0;
- for (AccountGroup.UUID groupUuid : newGroupUuids) {
- if (groupIndexer.reindexIfStale(groupUuid)) {
- reindexCounter++;
- }
+ AtomicInteger reindexCounter = new AtomicInteger();
+ List<ListenableFuture<?>> indexingTasks = new ArrayList<>();
+ for (AccountGroup.UUID groupUuid : allGroups) {
+ indexingTasks.add(
+ executor.submit(
+ () -> {
+ if (groupIndexer.reindexIfStale(groupUuid)) {
+ reindexCounter.incrementAndGet();
+ }
+ }));
}
- if (groupUuids != null) {
- // Check if any group was deleted since the last run and if yes remove these groups from the
- // index.
- for (AccountGroup.UUID groupUuid : Sets.difference(groupUuids, newGroupUuids)) {
- groupIndexer.index(groupUuid);
- reindexCounter++;
- }
+
+ Set<AccountGroup.UUID> groupsInIndex = queryAllGroupsFromIndex();
+ for (AccountGroup.UUID groupUuid : Sets.difference(groupsInIndex, allGroups)) {
+ indexingTasks.add(
+ executor.submit(
+ () -> {
+ groupIndexer.index(groupUuid);
+ reindexCounter.incrementAndGet();
+ }));
}
- groupUuids = newGroupUuids;
+ Futures.successfulAsList(indexingTasks).get();
logger.atInfo().log("Run group indexer, %s groups reindexed", reindexCounter);
} catch (Exception t) {
logger.atSevere().withCause(t).log("Failed to reindex groups");
}
}
+
+ private Set<AccountGroup.UUID> queryAllGroupsFromIndex() {
+ try {
+ DataSource<InternalGroup> result =
+ indexes
+ .getSearchIndex()
+ .getSource(
+ Predicate.any(),
+ QueryOptions.create(
+ indexConfig, 0, Integer.MAX_VALUE, Set.of(GroupField.UUID_FIELD.name())));
+ return StreamSupport.stream(result.readRaw().spliterator(), false)
+ .map(f -> fromUUIDField(f))
+ .collect(Collectors.toUnmodifiableSet());
+ } catch (QueryParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static AccountGroup.UUID fromUUIDField(FieldBundle f) {
+ return AccountGroup.uuid(f.<String>getValue(GroupField.UUID_FIELD_SPEC));
+ }
}
diff --git a/java/com/google/gerrit/server/group/db/GroupNameNotes.java b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
index 7c4fb16f9..3c7e012 100644
--- a/java/com/google/gerrit/server/group/db/GroupNameNotes.java
+++ b/java/com/google/gerrit/server/group/db/GroupNameNotes.java
@@ -167,6 +167,37 @@
}
/**
+ * Creates an instance of {@code GroupNameNotes} for use when deleting a new group.
+ *
+ * <p><strong>Note: </strong>The returned instance of {@code GroupNameNotes} has to be committed
+ * via {@link #commit(com.google.gerrit.server.git.meta.MetaDataUpdate) commit(MetaDataUpdate)} in
+ * order to delete the group.
+ *
+ * @param projectName the name of the project which holds the commits of the notes
+ * @param repository the repository which holds the commits of the notes
+ * @param groupUuid the UUID of the group to delete.
+ * @param groupName the name of the group to delete.
+ * @return an instance of {@code GroupNameNotes} configured for a specific group deletion
+ * @throws IOException if the repository can't be accessed for some reason
+ * @throws ConfigInvalidException in no case so far
+ */
+ public static GroupNameNotes forDeletingGroup(
+ Project.NameKey projectName,
+ Repository repository,
+ AccountGroup.UUID groupUuid,
+ AccountGroup.NameKey groupName)
+ throws ConfigInvalidException, IOException {
+ requireNonNull(groupName);
+ Optional<GroupReference> groupToDelete = loadGroup(repository, groupName);
+ if (groupToDelete.isEmpty()) {
+ throw new IOException("Could not load groups for deletion");
+ }
+ GroupNameNotes groupNameNotes = new GroupNameNotes(groupUuid, groupName, null);
+ groupNameNotes.load(projectName, repository);
+ return groupNameNotes;
+ }
+
+ /**
* Loads the {@code GroupReference} (name/UUID pair) for the group with the specified name.
*
* @param repository the repository which holds the commits of the notes
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index 31538d3..bcdb496 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.group.db;
import static com.google.gerrit.server.update.context.RefUpdateContext.RefUpdateType.GROUPS_UPDATE;
+import static org.eclipse.jgit.lib.Constants.R_REFS;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
@@ -28,8 +29,11 @@
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.entities.Project;
+import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.DuplicateKeyException;
import com.google.gerrit.exceptions.NoSuchGroupException;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.git.LockFailureException;
import com.google.gerrit.git.RefUpdateUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
@@ -62,8 +66,11 @@
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.BatchRefUpdate;
+import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
/**
* A database accessor for write calls related to groups.
@@ -307,6 +314,22 @@
}
}
+ /**
+ * Delete a specific group
+ *
+ * @param groupUuid the UUID of the group to delete
+ * @throws ConfigInvalidException if removing group note failed
+ * @throws IOException if indexing fails, or an error occurs while reading/writing from/to NoteDb
+ */
+ public void deleteGroup(AccountGroup.UUID groupUuid) throws ConfigInvalidException, IOException {
+ try (TraceTimer ignored =
+ TraceContext.newTimer(
+ "Deleting group", Metadata.builder().groupUuid(groupUuid.get()).build())) {
+ DeleteResult result = deleteGroupInNoteDbWithRetry(groupUuid);
+ evictCacheOnGroupDeletion(result);
+ }
+ }
+
private InternalGroup createGroupInNoteDbWithRetry(
InternalGroupCreation groupCreation, GroupDelta groupDelta)
throws IOException, ConfigInvalidException, DuplicateKeyException {
@@ -398,6 +421,76 @@
}
}
+ private DeleteResult deleteGroupInNoteDbWithRetry(AccountGroup.UUID groupUuid)
+ throws IOException, ConfigInvalidException {
+ try {
+ return retryHelper.groupUpdate("deleteGroup", () -> deleteGroupInNoteDb(groupUuid)).call();
+ } catch (Exception e) {
+ Throwables.throwIfUnchecked(e);
+ Throwables.throwIfInstanceOf(e, IOException.class);
+ Throwables.throwIfInstanceOf(e, ConfigInvalidException.class);
+ Throwables.throwIfInstanceOf(e, DuplicateKeyException.class);
+ throw new IOException(e);
+ }
+ }
+
+ private DeleteResult deleteGroupInNoteDb(AccountGroup.UUID groupUuid)
+ throws NoSuchGroupException, ConfigInvalidException, IOException {
+ try (RefUpdateContext ctx = RefUpdateContext.open(GROUPS_UPDATE)) {
+ try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) {
+ GroupConfig groupConfig = GroupConfig.loadForGroup(allUsersName, allUsersRepo, groupUuid);
+ if (groupConfig.getLoadedGroup().isEmpty()) {
+ throw new NoSuchGroupException(groupUuid);
+ }
+ InternalGroup group = groupConfig.getLoadedGroup().get();
+ GroupNameNotes groupNameNotes =
+ GroupNameNotes.forDeletingGroup(
+ allUsersName, allUsersRepo, groupUuid, group.getNameKey());
+ commit(allUsersRepo, null, groupNameNotes);
+ deleteSingleRefNote(group.getGroupUUID().get());
+ return buildDeleteResult(group);
+ }
+ }
+ }
+
+ private void deleteSingleRefNote(String ref) {
+ if (!ref.startsWith(R_REFS)) {
+ ref = RefNames.REFS_GROUPS + ref.substring(0, 2) + "/" + ref;
+ }
+ try (Repository repository = repoManager.openRepository(allUsersName)) {
+ RefUpdate u = repository.updateRef(ref);
+ u.setExpectedOldObjectId(repository.exactRef(ref).getObjectId());
+ u.setNewObjectId(ObjectId.zeroId());
+ u.setForceUpdate(true);
+ RefUpdate.Result result = u.delete();
+
+ switch (result) {
+ case NEW:
+ case NO_CHANGE:
+ case FAST_FORWARD:
+ case FORCED:
+ gitRefUpdated.fire(
+ allUsersName,
+ u,
+ ReceiveCommand.Type.DELETE,
+ currentUser.map(IdentifiedUser::state).orElse(null));
+ break;
+ case LOCK_FAILURE:
+ throw new LockFailureException(String.format("Cannot delete %s", ref), u);
+ case IO_FAILURE:
+ case NOT_ATTEMPTED:
+ case REJECTED:
+ case RENAMED:
+ case REJECTED_MISSING_OBJECT:
+ case REJECTED_OTHER_REASON:
+ default:
+ throw new StorageException(String.format("Cannot delete %s: %s", ref, result.name()));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
private static UpdateResult getUpdateResult(
InternalGroup originalGroup, InternalGroup updatedGroup) {
Set<Account.Id> addedMembers =
@@ -424,13 +517,31 @@
return resultBuilder.build();
}
+ private static DeleteResult buildDeleteResult(InternalGroup deletedGroup) {
+ ImmutableSet<Account.Id> deletedMembers = deletedGroup.getMembers();
+ ImmutableSet<AccountGroup.UUID> deletedSubgroups = deletedGroup.getSubgroups();
+
+ DeleteResult.Builder resultBuilder =
+ DeleteResult.builder()
+ .setDeletedGroupUuid(deletedGroup.getGroupUUID())
+ .setDeletedGroupId(deletedGroup.getId())
+ .setDeletedGroupName(deletedGroup.getNameKey())
+ .setDeletedGroupMembers(deletedMembers)
+ .setDeletedGroupSubgroups(deletedSubgroups);
+ return resultBuilder.build();
+ }
+
private void commit(
- Repository allUsersRepo, GroupConfig groupConfig, @Nullable GroupNameNotes groupNameNotes)
+ Repository allUsersRepo,
+ @Nullable GroupConfig groupConfig,
+ @Nullable GroupNameNotes groupNameNotes)
throws IOException {
BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
- try (MetaDataUpdate metaDataUpdate =
- metaDataUpdateFactory.create(allUsersName, allUsersRepo, batchRefUpdate)) {
- groupConfig.commit(metaDataUpdate);
+ if (groupConfig != null) {
+ try (MetaDataUpdate metaDataUpdate =
+ metaDataUpdateFactory.create(allUsersName, allUsersRepo, batchRefUpdate)) {
+ groupConfig.commit(metaDataUpdate);
+ }
}
if (groupNameNotes != null) {
// MetaDataUpdates unfortunately can't be reused. -> Create a new one.
@@ -475,6 +586,16 @@
result.getDeletedSubgroups().forEach(groupIncludeCache::evictParentGroupsOf);
}
+ private void evictCacheOnGroupDeletion(DeleteResult result) {
+ logger.atFine().log("evict caches on deletion of group %s", result.getDeletedGroupUuid());
+ groupCache.evict(result.getDeletedGroupUuid());
+ indexer.get().index(result.getDeletedGroupUuid());
+ groupCache.evict(result.getDeletedGroupId());
+ groupCache.evict(AccountGroup.nameKey(result.getDeletedGroupName().get()));
+ result.getDeletedGroupMembers().forEach(groupIncludeCache::evictGroupsWithMember);
+ result.getDeletedGroupSubgroups().forEach(groupIncludeCache::evictParentGroupsOf);
+ }
+
private void updateNameInProjectConfigsIfNecessary(UpdateResult result) {
if (result.getPreviousGroupName().isPresent()) {
AccountGroup.NameKey previousName = result.getPreviousGroupName().get();
@@ -597,4 +718,36 @@
abstract UpdateResult build();
}
}
+
+ @AutoValue
+ abstract static class DeleteResult {
+ abstract AccountGroup.UUID getDeletedGroupUuid();
+
+ abstract AccountGroup.Id getDeletedGroupId();
+
+ abstract AccountGroup.NameKey getDeletedGroupName();
+
+ abstract ImmutableSet<Account.Id> getDeletedGroupMembers();
+
+ abstract ImmutableSet<AccountGroup.UUID> getDeletedGroupSubgroups();
+
+ static Builder builder() {
+ return new AutoValue_GroupsUpdate_DeleteResult.Builder();
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder setDeletedGroupUuid(AccountGroup.UUID groupUuid);
+
+ abstract Builder setDeletedGroupId(AccountGroup.Id groupId);
+
+ abstract Builder setDeletedGroupName(AccountGroup.NameKey name);
+
+ abstract Builder setDeletedGroupMembers(Set<Account.Id> deletedMembers);
+
+ abstract Builder setDeletedGroupSubgroups(Set<AccountGroup.UUID> deletedSubgroups);
+
+ abstract DeleteResult build();
+ }
+ }
}
diff --git a/java/com/google/gerrit/server/index/account/AccountIndexCollection.java b/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
index 9d03e3b..8768fb6 100644
--- a/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
+++ b/java/com/google/gerrit/server/index/account/AccountIndexCollection.java
@@ -17,13 +17,23 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.entities.Account;
import com.google.gerrit.index.IndexCollection;
+import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.account.AccountState;
+import com.google.inject.Inject;
import com.google.inject.Singleton;
/** Collection of active account indices. See {@link IndexCollection} for details on collections. */
@Singleton
public class AccountIndexCollection
extends IndexCollection<Account.Id, AccountState, AccountIndex> {
+ @Inject
@VisibleForTesting
- public AccountIndexCollection() {}
+ public AccountIndexCollection(MetricMaker metrics) {
+ super(metrics);
+ }
+
+ @Override
+ protected IndexType getIndexName() {
+ return IndexType.ACCOUNTS;
+ }
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndex.java b/java/com/google/gerrit/server/index/change/ChangeIndex.java
index 74e9af1..cb44a3b 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndex.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndex.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.index.change;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.index.Index;
import com.google.gerrit.index.IndexDefinition;
import com.google.gerrit.index.query.Predicate;
@@ -35,4 +36,6 @@
}
Function<ChangeData, Change.Id> ENTITY_TO_KEY = ChangeData::getId;
+
+ public void deleteAllForProject(Project.NameKey project);
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java b/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java
index 817ec0e..75db96c 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndexCollection.java
@@ -17,12 +17,22 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.entities.Change;
import com.google.gerrit.index.IndexCollection;
+import com.google.gerrit.metrics.MetricMaker;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
import com.google.inject.Singleton;
/** Collection of active change indices. See {@link IndexCollection} for details on collections. */
@Singleton
public class ChangeIndexCollection extends IndexCollection<Change.Id, ChangeData, ChangeIndex> {
+ @Inject
@VisibleForTesting
- public ChangeIndexCollection() {}
+ public ChangeIndexCollection(MetricMaker metrics) {
+ super(metrics);
+ }
+
+ @Override
+ protected IndexType getIndexName() {
+ return IndexType.CHANGES;
+ }
}
diff --git a/java/com/google/gerrit/server/index/group/GroupIndexCollection.java b/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
index e1941ab..71f719d 100644
--- a/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
+++ b/java/com/google/gerrit/server/index/group/GroupIndexCollection.java
@@ -18,12 +18,22 @@
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.index.IndexCollection;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.inject.Inject;
import com.google.inject.Singleton;
/** Collection of active group indices. See {@link IndexCollection} for details on collections. */
@Singleton
public class GroupIndexCollection
extends IndexCollection<AccountGroup.UUID, InternalGroup, GroupIndex> {
+ @Inject
@VisibleForTesting
- public GroupIndexCollection() {}
+ public GroupIndexCollection(MetricMaker metrics) {
+ super(metrics);
+ }
+
+ @Override
+ protected IndexType getIndexName() {
+ return IndexType.GROUPS;
+ }
}
diff --git a/java/com/google/gerrit/server/logging/Metadata.java b/java/com/google/gerrit/server/logging/Metadata.java
index 33a49ae..60464e5 100644
--- a/java/com/google/gerrit/server/logging/Metadata.java
+++ b/java/com/google/gerrit/server/logging/Metadata.java
@@ -76,6 +76,9 @@
/** The cause of an error. */
public abstract Optional<String> cause();
+ /** The command name of an SSH request. */
+ public abstract Optional<String> commandName();
+
/** Side where the comment is written: <= 0 for parent, 1 for revision. */
public abstract Optional<Integer> commentSide();
@@ -88,6 +91,9 @@
/** The type of an event. */
public abstract Optional<String> eventType();
+ /** The name of an exception which failed an SSH request. */
+ public abstract Optional<String> exception();
+
/** The value of the @Export annotation which was used to register a plugin extension. */
public abstract Optional<String> exportValue();
@@ -192,8 +198,8 @@
* authDomainName=Optional.empty, branchName=Optional.empty, cacheKey=Optional.empty,
* cacheName=Optional.empty, caller=Optional.empty, className=Optional.empty,
* cancellationReason=Optional.empty, changeId=Optional[9212550], changeIdType=Optional.empty,
- * cause=Optional.empty, diffAlgorithm=Optional.empty, eventType=Optional.empty,
- * exportValue=Optional.empty, filePath=Optional.empty, garbageCollectorName=Optional.empty,
+ * cause=Optional.empty, commandName=Optional.empty, diffAlgorithm=Optional.empty, eventType=Optional.empty,
+ * exception=Optional.empty, exportValue=Optional.empty, filePath=Optional.empty, garbageCollectorName=Optional.empty,
* gitOperation=Optional.empty, groupId=Optional.empty, groupName=Optional.empty,
* groupUuid=Optional.empty, httpStatus=Optional.empty, indexName=Optional.empty,
* indexVersion=Optional[0], methodName=Optional.empty, multiple=Optional.empty,
@@ -235,10 +241,12 @@
.add("changeId", changeId().orElse(null))
.add("changeIdType", changeIdType().orElse(null))
.add("cause", cause().orElse(null))
+ .add("commandName", commandName().orElse(null))
.add("commentSide", commentSide().orElse(null))
.add("commit", commit().orElse(null))
.add("diffAlgorithm", diffAlgorithm().orElse(null))
.add("eventType", eventType().orElse(null))
+ .add("exception", exception().orElse(null))
.add("exportValue", exportValue().orElse(null))
.add("filePath", filePath().orElse(null))
.add("garbageCollectorName", garbageCollectorName().orElse(null))
@@ -314,6 +322,8 @@
public abstract Builder cause(@Nullable String cause);
+ public abstract Builder commandName(@Nullable String commandName);
+
public abstract Builder commentSide(int side);
public abstract Builder commit(@Nullable String commit);
@@ -322,6 +332,8 @@
public abstract Builder eventType(@Nullable String eventType);
+ public abstract Builder exception(@Nullable String exception);
+
public abstract Builder exportValue(@Nullable String exportValue);
public abstract Builder filePath(@Nullable String filePath);
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java b/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
index eb1f692..ba10648 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteFooters.java
@@ -22,12 +22,14 @@
public static final FooterKey FOOTER_BRANCH = new FooterKey("Branch");
public static final FooterKey FOOTER_CHANGE_ID = new FooterKey("Change-id");
public static final FooterKey FOOTER_COMMIT = new FooterKey("Commit");
+ public static final FooterKey FOOTER_CONTAINS_CONFLICTS = new FooterKey("Contains-Conflicts");
public static final FooterKey FOOTER_CURRENT = new FooterKey("Current");
public static final FooterKey FOOTER_CUSTOM_KEYED_VALUE = new FooterKey("Custom-Keyed-Value");
public static final FooterKey FOOTER_GROUPS = new FooterKey("Groups");
public static final FooterKey FOOTER_HASHTAGS = new FooterKey("Hashtags");
public static final FooterKey FOOTER_LABEL = new FooterKey("Label");
public static final FooterKey FOOTER_COPIED_LABEL = new FooterKey("Copied-Label");
+ public static final FooterKey FOOTER_OURS = new FooterKey("Ours");
public static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");
public static final FooterKey FOOTER_PATCH_SET_DESCRIPTION =
new FooterKey("Patch-set-description");
@@ -39,6 +41,7 @@
public static final FooterKey FOOTER_SUBMITTED_WITH = new FooterKey("Submitted-with");
public static final FooterKey FOOTER_TOPIC = new FooterKey("Topic");
public static final FooterKey FOOTER_TAG = new FooterKey("Tag");
+ public static final FooterKey FOOTER_THEIRS = new FooterKey("Theirs");
public static final FooterKey FOOTER_WORK_IN_PROGRESS = new FooterKey("Work-in-progress");
public static final FooterKey FOOTER_REVERT_OF = new FooterKey("Revert-of");
public static final FooterKey FOOTER_CHERRY_PICK_OF = new FooterKey("Cherry-pick-of");
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index f3ae867..b402d91 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -21,12 +21,14 @@
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHANGE_ID;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHERRY_PICK_OF;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COMMIT;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CONTAINS_CONFLICTS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COPIED_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CURRENT;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CUSTOM_KEYED_VALUE;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_GROUPS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_OURS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET_DESCRIPTION;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PRIVATE;
@@ -37,6 +39,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMISSION_ID;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMITTED_WITH;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TAG;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_THEIRS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TOPIC;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_WORK_IN_PROGRESS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.parseCommitMessageRange;
@@ -559,15 +562,15 @@
return;
}
- // Parse mutable patch set fields first so they can be recorded in the PendingPatchSetFields.
+ // Parse mutable patch set fields.
parseDescription(psId, commit);
parseGroups(psId, commit);
- ObjectId currRev = parseRevision(commit);
- if (currRev != null) {
- parsePatchSet(psId, currRev, accountId, realAccountId, commitTimestamp);
+ Optional<ObjectId> currRev = parseRevision(commit);
+ if (currRev.isPresent()) {
+ parsePatchSet(commit, psId, currRev.get(), accountId, realAccountId, commitTimestamp);
}
- parseCurrentPatchSet(psId, commit);
+ parseCurrentPatchSet(commit, psId);
if (status == null) {
status = parseStatus(commit);
@@ -683,23 +686,77 @@
return line;
}
- @Nullable
- private ObjectId parseRevision(ChangeNotesCommit commit) throws ConfigInvalidException {
- String sha = parseOneFooter(commit, FOOTER_COMMIT);
+ private Optional<ObjectId> parseRevision(ChangeNotesCommit commit) throws ConfigInvalidException {
+ return parseSha1(commit, FOOTER_COMMIT);
+ }
+
+ private Optional<ObjectId> parseOurs(ChangeNotesCommit commit) throws ConfigInvalidException {
+ return parseSha1(commit, FOOTER_OURS);
+ }
+
+ private Optional<ObjectId> parseTheirs(ChangeNotesCommit commit) throws ConfigInvalidException {
+ return parseSha1(commit, FOOTER_THEIRS);
+ }
+
+ private Optional<ObjectId> parseSha1(ChangeNotesCommit commit, FooterKey footerKey)
+ throws ConfigInvalidException {
+ String sha = parseOneFooter(commit, footerKey);
if (sha == null) {
- return null;
+ return Optional.empty();
}
try {
- return ObjectId.fromString(sha);
+ return Optional.of(ObjectId.fromString(sha));
} catch (InvalidObjectIdException e) {
- ConfigInvalidException cie = invalidFooter(FOOTER_COMMIT, sha);
+ ConfigInvalidException cie = invalidFooter(footerKey, sha);
cie.initCause(e);
throw cie;
}
}
+ private Optional<PatchSet.Conflicts> parseConflicts(ChangeNotesCommit commit)
+ throws ConfigInvalidException {
+ Optional<Boolean> containsConflicts = parseContainsConflicts(commit);
+ if (containsConflicts.isEmpty()) {
+ return Optional.empty();
+ }
+
+ Optional<ObjectId> ours = parseOurs(commit);
+ if (containsConflicts.get() && ours.isEmpty()) {
+ throw parseException(
+ "Missing footer: %s (if footer %s is set to true, footer %s must be set)",
+ FOOTER_OURS, FOOTER_CONTAINS_CONFLICTS.getName(), FOOTER_OURS);
+ }
+
+ Optional<ObjectId> theirs = parseTheirs(commit);
+ if (containsConflicts.get() && theirs.isEmpty()) {
+ throw parseException(
+ "Missing footer: %s (if footer %s is set to true, footer %s must be set)",
+ FOOTER_THEIRS, FOOTER_CONTAINS_CONFLICTS.getName(), FOOTER_THEIRS);
+ }
+
+ return Optional.of(PatchSet.Conflicts.create(ours, theirs, containsConflicts.get()));
+ }
+
+ private Optional<Boolean> parseContainsConflicts(ChangeNotesCommit commit)
+ throws ConfigInvalidException {
+ String containsConflictsStr = parseOneFooter(commit, FOOTER_CONTAINS_CONFLICTS);
+ if (containsConflictsStr == null) {
+ return Optional.empty();
+ } else if (Boolean.TRUE.toString().equalsIgnoreCase(containsConflictsStr)) {
+ return Optional.of(Boolean.TRUE);
+ } else if (Boolean.FALSE.toString().equalsIgnoreCase(containsConflictsStr)) {
+ return Optional.of(Boolean.FALSE);
+ }
+ throw invalidFooter(FOOTER_CONTAINS_CONFLICTS, containsConflictsStr);
+ }
+
private void parsePatchSet(
- PatchSet.Id psId, ObjectId rev, Account.Id accountId, Account.Id realAccountId, Instant ts)
+ ChangeNotesCommit commit,
+ PatchSet.Id psId,
+ ObjectId rev,
+ Account.Id accountId,
+ Account.Id realAccountId,
+ Instant ts)
throws ConfigInvalidException {
if (accountId == null) {
throw parseException("patch set %s requires an identified user as uploader", psId.get());
@@ -717,11 +774,12 @@
.commitId(rev)
.uploader(accountId)
.realUploader(realAccountId)
- .createdOn(ts);
+ .createdOn(ts)
+ .conflicts(parseConflicts(commit));
// Fields not set here:
- // * Groups, parsed earlier in parseGroups.
- // * Description, parsed earlier in parseDescription.
- // * Push certificate, parsed later in parseNotes.
+ // * Groups: parsed earlier in parseGroups.
+ // * Description: parsed earlier in parseDescription.
+ // * Push certificate: parsed later in parseNotes.
}
private void parseGroups(PatchSet.Id psId, ChangeNotesCommit commit)
@@ -737,7 +795,7 @@
}
}
- private void parseCurrentPatchSet(PatchSet.Id psId, ChangeNotesCommit commit)
+ private void parseCurrentPatchSet(ChangeNotesCommit commit, PatchSet.Id psId)
throws ConfigInvalidException {
// This commit implies a new current patch set if either it creates a new
// patch set, or sets the current field explicitly.
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index c97065b..ae99363 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -24,12 +24,14 @@
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHANGE_ID;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CHERRY_PICK_OF;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COMMIT;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CONTAINS_CONFLICTS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COPIED_LABEL;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CURRENT;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_CUSTOM_KEYED_VALUE;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_GROUPS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_OURS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PATCH_SET_DESCRIPTION;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_PRIVATE;
@@ -40,6 +42,7 @@
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMISSION_ID;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_SUBMITTED_WITH;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TAG;
+import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_THEIRS;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_TOPIC;
import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_WORK_IN_PROGRESS;
import static com.google.gerrit.server.notedb.NoteDbUtil.sanitizeFooter;
@@ -191,6 +194,7 @@
private boolean currentPatchSet;
private Boolean isPrivate;
private Boolean workInProgress;
+ private PatchSet.Conflicts conflicts;
private Integer revertOf;
// If null, the update does not modify the field. Otherwise, it updates the field with the
// new value or resets if cherryPickOf == Optional.empty().
@@ -891,6 +895,15 @@
}
}
+ if (conflicts != null) {
+ conflicts.ours().map(ObjectId::getName).ifPresent(ours -> addFooter(msg, FOOTER_OURS, ours));
+ conflicts
+ .theirs()
+ .map(ObjectId::getName)
+ .ifPresent(theirs -> addFooter(msg, FOOTER_THEIRS, theirs));
+ addFooter(msg, FOOTER_CONTAINS_CONFLICTS, conflicts.containsConflicts());
+ }
+
if (revertOf != null) {
addFooter(msg, FOOTER_REVERT_OF, revertOf);
}
@@ -1263,6 +1276,10 @@
this.workInProgress = workInProgress;
}
+ public void setConflicts(PatchSet.Conflicts conflicts) {
+ this.conflicts = conflicts;
+ }
+
@CanIgnoreReturnValue
private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) {
return sb.append(footer.getName()).append(": ");
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
index 677ee18..fe7d348 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackend.java
@@ -190,6 +190,7 @@
case CREATE_ACCOUNT:
case CREATE_GROUP:
+ case DELETE_GROUP:
case CREATE_PROJECT:
case MAINTAIN_SERVER:
case MODIFY_ACCOUNT:
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
index 1b87446..ae6cb9d 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionMappings.java
@@ -45,6 +45,7 @@
.put(GlobalPermission.ADMINISTRATE_SERVER, GlobalCapability.ADMINISTRATE_SERVER)
.put(GlobalPermission.CREATE_ACCOUNT, GlobalCapability.CREATE_ACCOUNT)
.put(GlobalPermission.CREATE_GROUP, GlobalCapability.CREATE_GROUP)
+ .put(GlobalPermission.DELETE_GROUP, GlobalCapability.DELETE_GROUP)
.put(GlobalPermission.CREATE_PROJECT, GlobalCapability.CREATE_PROJECT)
.put(GlobalPermission.EMAIL_REVIEWERS, GlobalCapability.EMAIL_REVIEWERS)
.put(GlobalPermission.FLUSH_CACHES, GlobalCapability.FLUSH_CACHES)
diff --git a/java/com/google/gerrit/server/permissions/GlobalPermission.java b/java/com/google/gerrit/server/permissions/GlobalPermission.java
index d83353c..ed1842d 100644
--- a/java/com/google/gerrit/server/permissions/GlobalPermission.java
+++ b/java/com/google/gerrit/server/permissions/GlobalPermission.java
@@ -43,6 +43,7 @@
ADMINISTRATE_SERVER,
CREATE_ACCOUNT,
CREATE_GROUP,
+ DELETE_GROUP,
CREATE_PROJECT,
EMAIL_REVIEWERS,
FLUSH_CACHES,
diff --git a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
index e586477..3cf55c5 100644
--- a/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
+++ b/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -34,10 +34,13 @@
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIdKeyFactory;
+import com.google.gerrit.server.config.AccountConfig;
import com.google.gerrit.server.index.account.AccountField;
import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.inject.Inject;
+import java.util.Arrays;
import java.util.List;
+import java.util.Locale;
import java.util.Set;
/**
@@ -49,6 +52,7 @@
public class InternalAccountQuery extends InternalQuery<AccountState, InternalAccountQuery> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+ private final AccountConfig accountConfig;
private final ExternalIdKeyFactory externalIdKeyFactory;
@Inject
@@ -56,8 +60,11 @@
AccountQueryProcessor queryProcessor,
AccountIndexCollection indexes,
IndexConfig indexConfig,
- ExternalIdKeyFactory externalIdKeyFactory) {
+ ExternalIdKeyFactory externalIdKeyFactory,
+ AccountConfig accountConfig) {
+
super(queryProcessor, indexes, indexConfig);
+ this.accountConfig = accountConfig;
this.externalIdKeyFactory = externalIdKeyFactory;
}
@@ -95,27 +102,36 @@
}
/**
- * Queries for accounts that have a preferred email that exactly matches the given email.
+ * Queries for accounts that have a preferred email that matches the given email.
+ *
+ * <p>The local part of the email is compared either in a case-insensitive or case-sensitive
+ * manner, depending on the configuration parameter {@code accounts.caseInsensitiveLocalPart}.
+ * Check the configuration documentation for more details.
*
* @param email preferred email by which accounts should be found
* @return list of accounts that have a preferred email that exactly matches the given email
*/
public List<AccountState> byPreferredEmail(String email) {
+ String normalizedEmail = normalizeEmailCase(email);
if (hasPreferredEmailExact()) {
- return query(AccountPredicates.preferredEmailExact(email));
+ return query(AccountPredicates.preferredEmailExact(normalizedEmail));
}
if (!hasPreferredEmail()) {
return ImmutableList.of();
}
- return query(AccountPredicates.preferredEmail(email)).stream()
- .filter(a -> a.account().preferredEmail().equals(email))
+ return query(AccountPredicates.preferredEmail(normalizedEmail)).stream()
+ .filter(a -> a.account().preferredEmail().equals(normalizedEmail))
.collect(toList());
}
/**
- * Makes multiple queries for accounts by preferred email (exact match).
+ * Makes multiple queries for accounts by preferred email.
+ *
+ * <p>The local part of the email is compared either in a case-insensitive or case-sensitive
+ * manner, depending on the configuration parameter {@code accounts.caseInsensitiveLocalPart}.
+ * Check the configuration documentation for more details.
*
* @param emails preferred emails by which accounts should be found
* @return multimap of the given emails to accounts that have a preferred email that exactly
@@ -124,7 +140,10 @@
public Multimap<String, AccountState> byPreferredEmail(List<String> emails) {
if (hasPreferredEmailExact()) {
List<List<AccountState>> r =
- query(emails.stream().map(AccountPredicates::preferredEmailExact).collect(toList()));
+ query(
+ emails.stream()
+ .map(email -> AccountPredicates.preferredEmailExact(normalizeEmailCase(email)))
+ .collect(toList()));
ListMultimap<String, AccountState> accountsByEmail = ArrayListMultimap.create();
for (int i = 0; i < emails.size(); i++) {
accountsByEmail.putAll(emails.get(i), r.get(i));
@@ -166,4 +185,21 @@
private boolean hasPreferredEmailExact() {
return hasField(AccountField.PREFERRED_EMAIL_EXACT_SPEC);
}
+
+ private String normalizeEmailCase(String email) {
+ return Arrays.asList(accountConfig.getCaseInsensitiveLocalParts())
+ .contains(getLowerCaseEmailDomain(email))
+ ? email.toLowerCase(Locale.US)
+ : email;
+ }
+
+ private String getLowerCaseEmailDomain(String email) {
+ String[] parts = email.split("@", 2);
+ // The caller method byPreferredEmail can be invoked with the local part
+ // of the email only. Handle this case by just returning it.
+ if (parts.length != 2) {
+ return email;
+ }
+ return parts[1].toLowerCase(Locale.US);
+ }
}
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalContext.java b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
index 971996d..5a26e62 100644
--- a/java/com/google/gerrit/server/query/approval/ApprovalContext.java
+++ b/java/com/google/gerrit/server/query/approval/ApprovalContext.java
@@ -22,6 +22,7 @@
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.extensions.client.ChangeKind;
import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.update.RepoView;
/** Entity representing all required information to match predicates for copying approvals. */
@@ -46,6 +47,9 @@
/** {@link ChangeNotes} of the change in question. */
public abstract ChangeNotes changeNotes();
+ /** {@link ChangeData } of the change in question. */
+ public abstract ChangeData changeData();
+
/** {@link ChangeKind} of the delta between the origin and target patch set. */
public abstract ChangeKind changeKind();
@@ -55,7 +59,7 @@
public abstract RepoView repoView();
public static ApprovalContext create(
- ChangeNotes changeNotes,
+ ChangeData changeData,
PatchSet.Id sourcePatchSetId,
Account.Id approverId,
LabelType labelType,
@@ -81,7 +85,8 @@
labelType,
approvalValue,
targetPatchSet,
- changeNotes,
+ changeData.notes(),
+ changeData,
changeKind,
isMerge,
repoView);
diff --git a/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java b/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java
index 6ae47ad..b86dcb7 100644
--- a/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/approval/ApprovalQueryBuilder.java
@@ -17,18 +17,25 @@
import static java.util.stream.Collectors.joining;
import com.google.common.base.Enums;
+import com.google.common.base.Splitter;
import com.google.common.primitives.Ints;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.index.query.Matchable;
+import com.google.gerrit.index.query.OperatorPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryBuilder;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.group.GroupResolver;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Arrays;
+import java.util.List;
import java.util.Locale;
import java.util.Optional;
@@ -37,11 +44,39 @@
private static final QueryBuilder.Definition<ApprovalContext, ApprovalQueryBuilder> mydef =
new QueryBuilder.Definition<>(ApprovalQueryBuilder.class);
+ private static final Splitter PLUGIN_SPLITTER = Splitter.on("_");
+
+ public static class ChangeIsPredicate extends OperatorPredicate<ApprovalContext>
+ implements Matchable<ApprovalContext> {
+ private final Predicate<ChangeData> delegate;
+
+ public ChangeIsPredicate(Predicate<ChangeData> delegate, String value) {
+ super("changeis", value);
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean match(ApprovalContext approvalContext) {
+ return delegate.asMatchable().match(approvalContext.changeData());
+ }
+
+ @Override
+ public int getCost() {
+ return delegate.asMatchable().getCost();
+ }
+ }
+
+ public interface UserInOperandFactory {
+ Predicate<ApprovalContext> create(UserInPredicate.Field field) throws QueryParseException;
+ }
+
private final MagicValuePredicate.Factory magicValuePredicate;
private final UserInPredicate.Factory userInPredicate;
private final GroupResolver groupResolver;
private final GroupControl.Factory groupControl;
private final ListOfFilesUnchangedPredicate listOfFilesUnchangedPredicate;
+ private final ChangeQueryBuilder changeQueryBuilder;
+ private final DynamicMap<UserInOperandFactory> userInOperands;
@Inject
protected ApprovalQueryBuilder(
@@ -49,13 +84,17 @@
UserInPredicate.Factory userInPredicate,
GroupResolver groupResolver,
GroupControl.Factory groupControl,
- ListOfFilesUnchangedPredicate listOfFilesUnchangedPredicate) {
+ ListOfFilesUnchangedPredicate listOfFilesUnchangedPredicate,
+ ChangeQueryBuilder changeQueryBuilder,
+ DynamicMap<UserInOperandFactory> userInOperands) {
super(mydef, null);
this.magicValuePredicate = magicValuePredicate;
this.userInPredicate = userInPredicate;
this.groupResolver = groupResolver;
this.groupControl = groupControl;
this.listOfFilesUnchangedPredicate = listOfFilesUnchangedPredicate;
+ this.changeQueryBuilder = changeQueryBuilder;
+ this.userInOperands = userInOperands;
}
@Operator
@@ -93,13 +132,15 @@
}
@Operator
- public Predicate<ApprovalContext> approverin(String group) throws QueryParseException {
- return userInPredicate.create(UserInPredicate.Field.APPROVER, parseGroupOrThrow(group));
+ public Predicate<ApprovalContext> approverin(String groupOrPluginOperand)
+ throws QueryParseException {
+ return userin(UserInPredicate.Field.APPROVER, groupOrPluginOperand);
}
@Operator
- public Predicate<ApprovalContext> uploaderin(String group) throws QueryParseException {
- return userInPredicate.create(UserInPredicate.Field.UPLOADER, parseGroupOrThrow(group));
+ public Predicate<ApprovalContext> uploaderin(String groupOrPluginOperand)
+ throws QueryParseException {
+ return userin(UserInPredicate.Field.UPLOADER, groupOrPluginOperand);
}
@Operator
@@ -114,6 +155,26 @@
value));
}
+ @Operator
+ public Predicate<ApprovalContext> changeis(String value) throws QueryParseException {
+ Predicate<ChangeData> changePredicate = changeQueryBuilder.is(value);
+ return new ChangeIsPredicate(changePredicate, value);
+ }
+
+ private Predicate<ApprovalContext> userin(
+ UserInPredicate.Field field, String groupOrPluginOperand) throws QueryParseException {
+ // For plugins the value will be operandName_pluginName
+ List<String> names = PLUGIN_SPLITTER.splitToList(groupOrPluginOperand);
+ if (names.size() == 2) {
+ UserInOperandFactory op = userInOperands.get(names.get(1), names.get(0));
+ if (op != null) {
+ return op.create(field);
+ }
+ }
+
+ return userInPredicate.create(field, parseGroupOrThrow(groupOrPluginOperand));
+ }
+
private static <T extends Enum<T>> Optional<T> parseEnumValue(Class<T> clazz, String value) {
return Optional.ofNullable(
Enums.getIfPresent(clazz, value.toUpperCase(Locale.US).replace('-', '_')).orNull());
diff --git a/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java b/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java
index 98471da..dc9a6d9 100644
--- a/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java
+++ b/java/com/google/gerrit/server/query/approval/MagicValuePredicate.java
@@ -26,7 +26,9 @@
enum MagicValue {
MIN,
MAX,
- ANY
+ ANY,
+ POSITIVE,
+ NEGATIVE
}
public interface Factory {
@@ -44,20 +46,20 @@
@Override
public boolean match(ApprovalContext ctx) {
- short pValue;
switch (value) {
case ANY:
return true;
case MIN:
- pValue = ctx.labelType().getMaxNegative();
- break;
+ return ctx.approvalValue() == ctx.labelType().getMaxNegative();
case MAX:
- pValue = ctx.labelType().getMaxPositive();
- break;
+ return ctx.approvalValue() == ctx.labelType().getMaxPositive();
+ case POSITIVE:
+ return ctx.approvalValue() > 0;
+ case NEGATIVE:
+ return ctx.approvalValue() < 0;
default:
throw new IllegalArgumentException("unrecognized label value: " + value);
}
- return pValue == ctx.approvalValue();
}
@Override
diff --git a/java/com/google/gerrit/server/query/approval/UserInPredicate.java b/java/com/google/gerrit/server/query/approval/UserInPredicate.java
index fda2014..10c072f 100644
--- a/java/com/google/gerrit/server/query/approval/UserInPredicate.java
+++ b/java/com/google/gerrit/server/query/approval/UserInPredicate.java
@@ -30,7 +30,7 @@
UserInPredicate create(Field field, AccountGroup.UUID group);
}
- enum Field {
+ public enum Field {
UPLOADER,
APPROVER
}
diff --git a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index 0fd9c0e..3ce7e38 100644
--- a/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -57,6 +57,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import org.eclipse.jgit.attributes.AttributesNodeProvider;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.io.DisabledOutputStream;
@@ -216,13 +217,15 @@
Map<Project.NameKey, Repository> repos = new HashMap<>();
Map<Project.NameKey, RevWalk> revWalks = new HashMap<>();
+ Map<Project.NameKey, AttributesNodeProvider> attributesNodeProviders = new HashMap<>();
QueryResult<ChangeData> results = queryProcessor.query(queryBuilder.parse(queryString));
pluginInfosByChange = queryProcessor.createPluginDefinedInfos(results.entities());
try {
AccountAttributeLoader accountLoader = accountAttributeLoaderFactory.create();
List<ChangeAttribute> changeAttributes = new ArrayList<>();
for (ChangeData d : results.entities()) {
- changeAttributes.add(buildChangeAttribute(d, repos, revWalks, accountLoader));
+ changeAttributes.add(
+ buildChangeAttribute(d, repos, revWalks, accountLoader, attributesNodeProviders));
}
accountLoader.fill();
changeAttributes.forEach(c -> show(c));
@@ -259,7 +262,8 @@
ChangeData d,
Map<Project.NameKey, Repository> repos,
Map<Project.NameKey, RevWalk> revWalks,
- AccountAttributeLoader accountLoader)
+ AccountAttributeLoader accountLoader,
+ Map<Project.NameKey, AttributesNodeProvider> attributesNodeProviders)
throws IOException {
ChangeAttribute c = eventFactory.asChangeAttribute(d.change(), accountLoader);
c.hashtags = Lists.newArrayList(d.hashtags());
@@ -287,21 +291,26 @@
if (includePatchSets || includeCurrentPatchSet || includeDependencies) {
Project.NameKey p = d.change().getProject();
Repository repo;
+ AttributesNodeProvider attributesNodeProvider;
RevWalk rw = revWalks.get(p);
- // Cache and reuse repos and revwalks.
+ // Cache and reuse repos, revWalks, and attributesNodeProviders.
if (rw == null) {
repo = repoManager.openRepository(p);
checkState(repos.put(p, repo) == null);
rw = new RevWalk(repo);
revWalks.put(p, rw);
+ attributesNodeProvider = repo.createAttributesNodeProvider();
+ attributesNodeProviders.put(p, attributesNodeProvider);
} else {
repo = repos.get(p);
+ attributesNodeProvider = attributesNodeProviders.get(p);
}
if (includePatchSets) {
eventFactory.addPatchSets(
rw,
repo.getConfig(),
+ attributesNodeProvider,
c,
includeApprovals ? d.conditionallyLoadApprovalsWithCopied().asMap() : null,
includeFiles,
@@ -328,7 +337,13 @@
}
} else {
c.currentPatchSet =
- eventFactory.asPatchSetAttribute(rw, repo.getConfig(), d, current, accountLoader);
+ eventFactory.asPatchSetAttribute(
+ rw,
+ repo.getConfig(),
+ repo.createAttributesNodeProvider(),
+ d,
+ current,
+ accountLoader);
if (includeFiles) {
eventFactory.addPatchSetFileNames(c.currentPatchSet, d.change(), d.currentPatchSet());
}
diff --git a/java/com/google/gerrit/server/restapi/RestApiModule.java b/java/com/google/gerrit/server/restapi/RestApiModule.java
index 359393e..3bc5bb8 100644
--- a/java/com/google/gerrit/server/restapi/RestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/RestApiModule.java
@@ -32,6 +32,7 @@
* bound in {@link RestModule}.
*/
public class RestApiModule extends AbstractModule {
+
@Override
protected void configure() {
install(new AccessRestApiModule());
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 5402dcc..13c41cf 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -394,7 +394,8 @@
input.parent - 1,
input.allowEmpty,
input.allowConflicts,
- useDiff3);
+ useDiff3,
+ git.createAttributesNodeProvider());
logger.atFine().log("flushing inserter %s", oi);
oi.flush();
} catch (MergeIdenticalTreeException | MergeConflictException e) {
@@ -470,6 +471,7 @@
inserter.setMessage(
messageForDestinationChange(
inserter.getPatchSetId(), sourceBranch, sourceCommit, cherryPickCommit));
+ cherryPickCommit.getConflicts().ifPresent(inserter::setConflicts);
inserter.setTopic(topic);
if (workInProgress != null) {
inserter.setWorkInProgress(workInProgress);
@@ -511,6 +513,7 @@
throws IOException, InvalidChangeOperationException {
Change.Id changeId = idForNewChange != null ? idForNewChange : Change.id(seq.nextChangeId());
ChangeInserter ins = changeInserterFactory.create(changeId, cherryPickCommit, refName);
+ cherryPickCommit.getConflicts().ifPresent(ins::setConflicts);
ins.setRevertOf(revertOf);
if (workInProgress != null) {
ins.setWorkInProgress(workInProgress);
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 2c32586..57be320 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -146,6 +146,7 @@
private final NotifyResolver notifyResolver;
private final ContributorAgreementsChecker contributorAgreements;
private final boolean disablePrivateChanges;
+ private final boolean useDiff3;
@Inject
CreateChange(
@@ -186,6 +187,9 @@
this.mergeUtilFactory = mergeUtilFactory;
this.notifyResolver = notifyResolver;
this.contributorAgreements = contributorAgreements;
+ this.useDiff3 =
+ config.getBoolean(
+ "change", /* subsection= */ null, "diff3ConflictView", /* defaultValue= */ false);
}
@Override
@@ -533,6 +537,7 @@
ins.setPrivate(input.isPrivate);
ins.setWorkInProgress(input.workInProgress || !c.getFilesWithGitConflicts().isEmpty());
ins.setGroups(groups);
+ c.getConflicts().ifPresent(ins::setConflicts);
if (input.validationOptions != null) {
ImmutableListMultimap.Builder<String, String> validationOptions =
@@ -702,9 +707,12 @@
ObjectId treeId = mergeTip == null ? emptyTreeId(oi) : mergeTip.getTree().getId();
logger.atFine().log("Tree ID of empty commit: %s", treeId.name());
List<RevCommit> parents = mergeTip == null ? ImmutableList.of() : ImmutableList.of(mergeTip);
- return rw.parseCommit(
- CommitUtil.createCommitWithTree(
- oi, authorIdent, committerIdent, parents, commitMessage, treeId));
+ CodeReviewCommit commit =
+ rw.parseCommit(
+ CommitUtil.createCommitWithTree(
+ oi, authorIdent, committerIdent, parents, commitMessage, treeId));
+ commit.setNoConflicts();
+ return commit;
}
private static CodeReviewCommit createCommitWithSuppliedTree(
@@ -780,7 +788,8 @@
authorIdent,
committerIdent,
commitMessage,
- rw);
+ rw,
+ this.useDiff3);
logger.atFine().log("tree ID of merge commit: %s", mergeCommit.getTree().getId().name());
return mergeCommit;
} catch (NoMergeBaseException e) {
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index 87d983e..19ba6d1 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -50,6 +50,7 @@
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.change.PatchSetInserter;
+import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.GitRepositoryManager;
@@ -76,6 +77,7 @@
import java.time.ZoneId;
import java.util.List;
import java.util.Optional;
+import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
@@ -99,9 +101,11 @@
private final ProjectCache projectCache;
private final ChangeFinder changeFinder;
private final PermissionBackend permissionBackend;
+ private final boolean useDiff3;
@Inject
CreateMergePatchSet(
+ @GerritServerConfig Config cfg,
BatchUpdate.Factory updateFactory,
GitRepositoryManager gitManager,
CommitsCollection commits,
@@ -126,6 +130,9 @@
this.projectCache = projectCache;
this.changeFinder = changeFinder;
this.permissionBackend = permissionBackend;
+ this.useDiff3 =
+ cfg.getBoolean(
+ "change", /* subsection= */ null, "diff3ConflictView", /* defaultValue= */ false);
}
@Override
@@ -222,6 +229,7 @@
.setMessage(messageForChange(nextPsId, newCommit))
.setWorkInProgress(!newCommit.getFilesWithGitConflicts().isEmpty())
.setCheckAddPatchSetPermission(false);
+ newCommit.getConflicts().ifPresent(psInserter::setConflicts);
if (in.validationOptions != null) {
ImmutableListMultimap.Builder<String, String> validationOptions =
@@ -322,7 +330,8 @@
author,
committer,
commitMsg,
- rw);
+ rw,
+ this.useDiff3);
}
private static String messageForChange(PatchSet.Id patchSetId, CodeReviewCommit commit) {
diff --git a/java/com/google/gerrit/server/restapi/change/GetContent.java b/java/com/google/gerrit/server/restapi/change/GetContent.java
index c536946..5ce10b4 100644
--- a/java/com/google/gerrit/server/restapi/change/GetContent.java
+++ b/java/com/google/gerrit/server/restapi/change/GetContent.java
@@ -66,8 +66,9 @@
public Response<BinaryResult> apply(FileResource rsrc)
throws ResourceNotFoundException, IOException, BadRequestException {
String path = rsrc.getPatchKey().fileName();
+ PatchSet.Id patchSetId = rsrc.getRevision().getPatchSet().id();
if (Patch.COMMIT_MSG.equals(path)) {
- String msg = getMessage(rsrc.getRevision().getChangeResource().getNotes());
+ String msg = getMessage(rsrc.getRevision().getChangeResource().getNotes(), patchSetId);
return Response.ok(
BinaryResult.create(msg)
.setContentType(FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE)
@@ -89,9 +90,9 @@
parent));
}
- private String getMessage(ChangeNotes notes) throws IOException {
+ private String getMessage(ChangeNotes notes, PatchSet.Id patchSetId) throws IOException {
Change.Id changeId = notes.getChangeId();
- PatchSet ps = psUtil.current(notes);
+ PatchSet ps = psUtil.get(notes, patchSetId);
if (ps == null) {
throw new NoSuchChangeException(changeId);
}
diff --git a/java/com/google/gerrit/server/restapi/change/GetPatch.java b/java/com/google/gerrit/server/restapi/change/GetPatch.java
index 238712e..05f061d 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPatch.java
@@ -43,10 +43,29 @@
public class GetPatch implements RestReadView<RevisionResource> {
private final GitRepositoryManager repoManager;
- @Option(name = "--zip")
+ /**
+ * What is this base64 and zip business doing here? Just give me a patch file!
+ *
+ * <p>The reason these legacy types are here is to force paleolithic browsers like IE6 to not do
+ * cross site scripting. We have since invented X-Content-Type-Options: nosniff, which every
+ * browser released since IE8 supports, making this madness unnecessary in the modern era, thus
+ * the raw mode being available.
+ *
+ * <p>The only reason raw is not default is to not break old scripts.
+ */
+ private enum OutputType {
+ ZIP,
+ BASE64,
+ RAW,
+ }
+
+ @Option(name = "--zip", usage = "retrieve a zip file with one patch file inside it")
private boolean zip;
- @Option(name = "--download")
+ @Option(name = "--raw", usage = "retrieve a plain-text patch file rather than base64")
+ private boolean raw;
+
+ @Option(name = "--download", usage = "send the file with a download hint")
private boolean download;
@Option(name = "--path")
@@ -67,6 +86,18 @@
ResourceConflictException,
IOException,
ResourceNotFoundException {
+ if (raw && zip) {
+ throw new BadRequestException("raw and zip options are mutually exclusive");
+ }
+ final OutputType outputType;
+ if (raw) {
+ outputType = OutputType.RAW;
+ } else if (zip) {
+ outputType = OutputType.ZIP;
+ } else {
+ outputType = OutputType.BASE64;
+ }
+
final Repository repo = repoManager.openRepository(rsrc.getProject());
boolean close = true;
try {
@@ -91,16 +122,20 @@
new BinaryResult() {
@Override
public void writeTo(OutputStream out) throws IOException {
- if (zip) {
- ZipOutputStream zos = new ZipOutputStream(out);
- ZipEntry e = new ZipEntry(fileName(rw, commit));
- e.setTime(commit.getCommitTime() * 1000L);
- zos.putNextEntry(e);
- format(zos);
- zos.closeEntry();
- zos.finish();
- } else {
- format(out);
+ switch (outputType) {
+ case ZIP:
+ ZipOutputStream zos = new ZipOutputStream(out);
+ ZipEntry e = new ZipEntry(fileName(rw, commit));
+ e.setTime(commit.getCommitTime() * 1000L);
+ zos.putNextEntry(e);
+ format(zos);
+ zos.closeEntry();
+ zos.finish();
+ break;
+ case RAW:
+ case BASE64:
+ format(out);
+ break;
}
}
@@ -123,14 +158,21 @@
throw new ResourceNotFoundException(String.format("File not found: %s.", path));
}
- if (zip) {
- bin.disableGzip()
- .setContentType("application/zip")
- .setAttachmentName(fileName(rw, commit) + ".zip");
- } else {
- bin.base64()
- .setContentType("application/mbox")
- .setAttachmentName(download ? fileName(rw, commit) + ".base64" : null);
+ switch (outputType) {
+ case ZIP:
+ bin.disableGzip()
+ .setContentType("application/zip")
+ .setAttachmentName(fileName(rw, commit) + ".zip");
+ break;
+ case BASE64:
+ bin.base64()
+ .setContentType("application/mbox")
+ .setAttachmentName(download ? fileName(rw, commit) + ".base64" : null);
+ break;
+ case RAW:
+ bin.setContentType("text/plain")
+ .setAttachmentName(download ? fileName(rw, commit) : null);
+ break;
}
close = false;
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
index 819ae72..1482144 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewerRecommender.java
@@ -14,10 +14,12 @@
package com.google.gerrit.server.restapi.change;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static java.util.stream.Collectors.toList;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -48,7 +50,6 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@@ -203,7 +204,7 @@
int numberOfRelevantChanges = config.getInt("suggest", "relevantChanges", 50);
// Get the user's last numberOfRelevantChanges changes, check reviewers
try {
- ImmutableList<ChangeData> result =
+ ImmutableList<ChangeData> changes =
queryProvider
.get()
.setLimit(numberOfRelevantChanges)
@@ -213,9 +214,13 @@
// Put those candidates at the bottom of the list
candidateList.stream().forEach(id -> suggestions.put(id, new MutableDouble(0)));
- for (ChangeData cd : result) {
+ ImmutableSet<Account.Id> candidateIds =
+ changes.stream().flatMap(cd -> cd.reviewers().all().stream()).collect(toImmutableSet());
+ Map<Account.Id, AccountState> candidateStates = accountCache.get(candidateIds);
+
+ for (ChangeData cd : changes) {
for (Account.Id reviewer : cd.reviewers().all()) {
- if (accountMatchesQuery(reviewer, query)) {
+ if (accountMatchesQuery(candidateStates.get(reviewer), query)) {
suggestions
.computeIfAbsent(reviewer, (ignored) -> new MutableDouble(0))
.add(baseWeight);
@@ -230,13 +235,15 @@
}
}
- private boolean accountMatchesQuery(Account.Id id, String query) {
- Optional<Account> account = accountCache.get(id).map(AccountState::account);
- if (account.isPresent() && account.get().isActive()) {
+ private boolean accountMatchesQuery(AccountState accountState, String query) {
+ if (accountState == null) {
+ return false;
+ }
+ Account account = accountState.account();
+ if (account.isActive()) {
if (Strings.isNullOrEmpty(query)
- || (account.get().fullName() != null && account.get().fullName().startsWith(query))
- || (account.get().preferredEmail() != null
- && account.get().preferredEmail().startsWith(query))) {
+ || (account.fullName() != null && account.fullName().startsWith(query))
+ || (account.preferredEmail() != null && account.preferredEmail().startsWith(query))) {
return true;
}
}
diff --git a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
index 93173128b..29f76ee 100644
--- a/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ReviewersUtil.java
@@ -350,7 +350,9 @@
private List<SuggestedReviewerInfo> loadAccounts(List<Account.Id> accountIds)
throws PermissionBackendException {
Set<FillOptions> fillOptions =
- Sets.union(AccountLoader.DETAILED_OPTIONS, EnumSet.of(FillOptions.SECONDARY_EMAILS));
+ Sets.union(
+ AccountLoader.DETAILED_OPTIONS_WITHOUT_AVATAR,
+ EnumSet.of(FillOptions.SECONDARY_EMAILS));
AccountLoader accountLoader = accountLoaderFactory.create(fillOptions);
try (Timer0.Context ctx = metrics.loadAccountsLatency.start()) {
diff --git a/java/com/google/gerrit/server/restapi/group/DeleteGroup.java b/java/com/google/gerrit/server/restapi/group/DeleteGroup.java
new file mode 100644
index 0000000..6954dd0
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/group/DeleteGroup.java
@@ -0,0 +1,200 @@
+// Copyright (C) 2024 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.group;
+
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.CachedProjectConfig;
+import com.google.gerrit.entities.GroupDescription;
+import com.google.gerrit.entities.InternalGroup;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.extensions.client.ListGroupsOption;
+import com.google.gerrit.extensions.common.DeleteGroupInput;
+import com.google.gerrit.extensions.common.GroupInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.UserInitiated;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.GroupResource;
+import com.google.gerrit.server.group.db.Groups;
+import com.google.gerrit.server.group.db.GroupsUpdate;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+public class DeleteGroup implements RestModifyView<GroupResource, DeleteGroupInput> {
+ private final Provider<ListGroups> listGroupProvider;
+ private final GroupCache groupCache;
+ private final ProjectCache projectCache;
+ private final Provider<GroupsUpdate> groupsUpdateProvider;
+ private final GroupJson json;
+ private final Groups groups;
+ private final boolean deleteGroupEnabled;
+
+ @Inject
+ DeleteGroup(
+ Provider<ListGroups> listGroupProvider,
+ GroupCache groupCache,
+ ProjectCache projectCache,
+ @UserInitiated Provider<GroupsUpdate> groupsUpdateProvider,
+ @GerritServerConfig Config cfg,
+ GroupJson json,
+ Groups groups) {
+ this.listGroupProvider = listGroupProvider;
+ this.groupCache = groupCache;
+ this.projectCache = projectCache;
+ this.groupsUpdateProvider = groupsUpdateProvider;
+ this.json = json;
+ this.groups = groups;
+ this.deleteGroupEnabled = cfg.getBoolean("groups", "enableDeleteGroup", false);
+ }
+
+ public DeleteGroup addOption(ListGroupsOption o) {
+ json.addOption(o);
+ return this;
+ }
+
+ @Override
+ public Response<String> apply(GroupResource resource, DeleteGroupInput input)
+ throws AuthException,
+ BadRequestException,
+ UnprocessableEntityException,
+ ResourceConflictException,
+ IOException,
+ ConfigInvalidException,
+ ResourceNotFoundException,
+ PermissionBackendException,
+ NotInternalGroupException {
+ if (deleteGroupEnabled) {
+ GroupDescription.Internal internalGroup =
+ resource.asInternalGroup().orElseThrow(NotInternalGroupException::new);
+ final GroupControl control = resource.getControl();
+ if (!control.canDeleteGroup()) {
+ throw new AuthException("Cannot delete group " + internalGroup.getName());
+ }
+ groupDeletionPrecondition(internalGroup);
+ deleteGroup(internalGroup);
+ return Response.ok();
+ }
+ throw new ResourceNotFoundException("Deletion of Group is not enabled");
+ }
+
+ private void groupDeletionPrecondition(GroupDescription.Internal internalGroup)
+ throws ResourceConflictException, ConfigInvalidException, IOException {
+ AccountGroup.UUID uuid = internalGroup.getGroupUUID();
+ if (groupCache.get(internalGroup.getGroupUUID()).isEmpty()) {
+ throw new ResourceConflictException(
+ String.format("group %s does not exist", internalGroup.getGroupUUID()));
+ }
+ List<InternalGroup> ownedGroup = getOwnedGroup(uuid, internalGroup.getName());
+ if (!ownedGroup.isEmpty()) {
+ String msg =
+ "Cannot delete group that is owner of other groups: \n"
+ + ownedGroup.stream()
+ .map(InternalGroup::getName)
+ .collect(Collectors.joining(", ", "[", "]"));
+ throw new ResourceConflictException(msg);
+ }
+ List<String> inProjects = getProjectsWithGroupRefs(uuid);
+ if (!inProjects.isEmpty()) {
+ String msg =
+ "Cannot delete group that is referenced in access permissions for project: \n"
+ + inProjects;
+ throw new ResourceConflictException(msg);
+ }
+ List<String> subgroupsInGroups = getSubgroupsInGroups(uuid, internalGroup.getName());
+ if (!subgroupsInGroups.isEmpty()) {
+ String msg = "Cannot delete group that is subgroup of another group: \n" + subgroupsInGroups;
+ throw new ResourceConflictException(msg);
+ }
+ }
+
+ private List<InternalGroup> getOwnedGroup(AccountGroup.UUID uuid, String groupOwnerName)
+ throws IOException {
+ try {
+ ListGroups listOwner = listGroupProvider.get();
+ listOwner.setOwnedBy(uuid.get());
+ List<GroupInfo> groups = listOwner.get();
+ return groups.stream()
+ .filter(group -> !group.name.equals(groupOwnerName))
+ .map(group -> groupCache.get(AccountGroup.UUID.parse(group.id)))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .collect(Collectors.toList());
+ } catch (Exception e) {
+ throw new IOException("Failed to check owned groups for group " + uuid.get(), e);
+ }
+ }
+
+ private List<String> getProjectsWithGroupRefs(AccountGroup.UUID uuid) {
+ List<String> projects = new ArrayList<>();
+ for (Project.NameKey projectName : projectCache.all()) {
+ Optional<ProjectState> projectState = projectCache.get(projectName);
+ if (projectState.isPresent()) {
+ CachedProjectConfig config = projectState.get().getConfig();
+ if (config.getGroup(uuid).isPresent()) {
+ projects.add(projectName.toString());
+ }
+ }
+ }
+ return projects;
+ }
+
+ private List<String> getSubgroupsInGroups(AccountGroup.UUID uuid, String groupName)
+ throws ConfigInvalidException, IOException {
+ List<String> allGroupsWithSubGroups = new ArrayList<>();
+ groups
+ .getAllGroupReferences()
+ .forEach(
+ entry -> {
+ try {
+ if (groups.getGroup(entry.getUUID()).isEmpty()) {
+ throw new ResourceNotFoundException(
+ String.format(
+ "Could not check if group %s is subgroup of %s",
+ groupName, entry.getName()));
+ }
+ if (groups.getGroup(entry.getUUID()).get().getSubgroups().contains(uuid)) {
+ allGroupsWithSubGroups.add(entry.getName());
+ }
+ } catch (IOException | ConfigInvalidException | ResourceNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ return allGroupsWithSubGroups;
+ }
+
+ private void deleteGroup(GroupDescription.Internal internalGroup)
+ throws IOException, ConfigInvalidException {
+ AccountGroup.UUID uuid = internalGroup.getGroupUUID();
+ groupsUpdateProvider.get().deleteGroup(uuid);
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/group/GroupRestApiModule.java b/java/com/google/gerrit/server/restapi/group/GroupRestApiModule.java
index f115374..e9f726c 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupRestApiModule.java
@@ -38,6 +38,7 @@
DynamicMap.mapOf(binder(), SUBGROUP_KIND);
create(GROUP_KIND).to(CreateGroup.class);
+ delete(GROUP_KIND).to(DeleteGroup.class);
get(GROUP_KIND).to(GetGroup.class);
put(GROUP_KIND).to(PutGroup.class);
get(GROUP_KIND, "description").to(GetDescription.class);
diff --git a/java/com/google/gerrit/server/restapi/project/CheckMergeability.java b/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
index dc4e6df..6b46164 100644
--- a/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
+++ b/java/com/google/gerrit/server/restapi/project/CheckMergeability.java
@@ -45,6 +45,7 @@
private String source;
private String strategy;
private SubmitType submitType;
+ private final boolean useGitattributesForMerge;
@Option(
name = "--source",
@@ -76,6 +77,7 @@
this.commits = commits;
this.strategy = MergeUtil.getMergeStrategy(cfg).getName();
this.submitType = cfg.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
+ this.useGitattributesForMerge = MergeUtil.useGitattributesForMerge(cfg);
}
@Override
@@ -95,8 +97,6 @@
try (Repository git = gitManager.openRepository(resource.getNameKey());
RevWalk rw = new RevWalk(git);
ObjectInserter inserter = new InMemoryInserter(git)) {
- Merger m = MergeUtil.newMerger(inserter, git.getConfig(), strategy);
-
Ref destRef = git.getRefDatabase().exactRef(resource.getRef());
if (destRef == null) {
throw new ResourceNotFoundException(resource.getRef());
@@ -126,6 +126,12 @@
return Response.ok(result);
}
+ Merger m = MergeUtil.newMerger(inserter, git.getConfig(), strategy);
+ if (m instanceof ResolveMerger && useGitattributesForMerge) {
+ // We need to set the attributes provider before attempting the merge in order to read and
+ // honor gitattributes merge settings correctly
+ ((ResolveMerger) m).setAttributesNodeProvider(git.createAttributesNodeProvider());
+ }
if (m.merge(false, targetCommit, sourceCommit)) {
result.mergeable = true;
result.commitMerged = false;
diff --git a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
index 61e3c3c..7cc4d75 100644
--- a/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
+++ b/java/com/google/gerrit/server/restapi/project/RepoMetaDataUpdater.java
@@ -343,7 +343,7 @@
.setMessage(
// Same message as in ReceiveCommits.CreateRequest.
ApprovalsUtil.renderMessageWithApprovals(1, ImmutableMap.of(), ImmutableMap.of()))
- .setValidate(false)
+ .disableValidation()
.setUpdateRef(false);
}
}
diff --git a/java/com/google/gerrit/server/submit/CherryPick.java b/java/com/google/gerrit/server/submit/CherryPick.java
index a213f28..002d0a1 100644
--- a/java/com/google/gerrit/server/submit/CherryPick.java
+++ b/java/com/google/gerrit/server/submit/CherryPick.java
@@ -122,7 +122,8 @@
0,
false,
false,
- useDiff3);
+ useDiff3,
+ ctx.getRepoView().getAttributesNodeProvider());
} catch (MergeConflictException mce) {
// Keep going in the case of a single merge failure; the goal is to
// cherry-pick as many commits as possible.
@@ -213,7 +214,8 @@
ctx.getRepoView().getConfig(),
args.destBranch,
mergeTip.getCurrentTip(),
- toMerge);
+ toMerge,
+ ctx.getRepoView().getAttributesNodeProvider());
result = amendGitlink(result);
mergeTip.moveTipTo(result, toMerge);
args.mergeUtil.markCleanMerges(
diff --git a/java/com/google/gerrit/server/submit/MergeOneOp.java b/java/com/google/gerrit/server/submit/MergeOneOp.java
index 1840479..d2b6ea1 100644
--- a/java/com/google/gerrit/server/submit/MergeOneOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOneOp.java
@@ -46,7 +46,8 @@
ctx.getRepoView().getConfig(),
args.destBranch,
args.mergeTip.getCurrentTip(),
- toMerge);
+ toMerge,
+ ctx.getRepoView().getAttributesNodeProvider());
if (args.project.is(BooleanProjectConfig.REJECT_EMPTY_COMMIT)
&& merged.getTree().equals(merged.getParent(0).getTree())) {
toMerge.setStatusCode(EMPTY_COMMIT);
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 1f7288d..74ea315 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -474,22 +474,37 @@
// MergeSuperSetComputation is an interface and on API level doesn't guarantee that this
// have been verified for all changes. Additionally, this protects against potential
// issues due to staleness.
- logger.atFine().log(
- "Change %d cannot be submitted by user %s because it depends on change %d which the"
- + "user cannot read",
- triggeringChangeId.get(), caller.getRealUser().getLoggableName(), cd.getId().get());
- problems.add(
- ChangeProblem.create(
- cd.getId(),
- String.format(
- "Change %d depends on other hidden changes", triggeringChangeId.get())));
+ if (triggeringChangeId.get() != cd.getId().get()) {
+ logger.atFine().log(
+ "Change %d cannot be submitted by user %s because it depends on change %d which the"
+ + "user cannot read",
+ triggeringChangeId.get(), caller.getRealUser().getLoggableName(), cd.getId().get());
+ problems.add(
+ ChangeProblem.create(
+ cd.getId(),
+ String.format(
+ "Change %d depends on other hidden changes", triggeringChangeId.get())));
+ } else {
+ logger.atFine().log(
+ "Change %d cannot be submitted by user %s because they don't have READ permission",
+ triggeringChangeId.get(), caller.getRealUser().getLoggableName());
+ problems.add(
+ ChangeProblem.create(
+ cd.getId(), String.format("Change %d is not visible", triggeringChangeId.get())));
+ }
return;
}
if (!can.contains(ChangePermission.SUBMIT)) {
- logger.atFine().log(
- "Change %d cannot be submitted by user %s because it depends on change %d which the"
- + "user cannot submit",
- triggeringChangeId.get(), caller.getRealUser().getLoggableName(), cd.getId().get());
+ if (triggeringChangeId.get() != cd.getId().get()) {
+ logger.atFine().log(
+ "Change %d cannot be submitted by user %s because it depends on change %d which the"
+ + "user cannot submit",
+ triggeringChangeId.get(), caller.getRealUser().getLoggableName(), cd.getId().get());
+ } else {
+ logger.atFine().log(
+ "Change %d cannot be submitted by user %s because they don't have SUBMIT permission",
+ triggeringChangeId.get(), caller.getRealUser().getLoggableName());
+ }
problems.add(
ChangeProblem.create(
cd.getId(),
@@ -498,13 +513,22 @@
}
if (caller.isImpersonating()) {
if (!permissionBackend.user(caller).change(cd).test(ChangePermission.READ)) {
- logger.atFine().log(
- "Change %d cannot be submitted by user %s on behalf of user %s because it depends on"
- + " change %d which the on-behalf-of user does not have READ permission for",
- triggeringChangeId.get(),
- caller.getRealUser().getLoggableName(),
- caller.getLoggableName(),
- cd.getId().get());
+ if (triggeringChangeId.get() != cd.getId().get()) {
+ logger.atFine().log(
+ "Change %d cannot be submitted by user %s on behalf of user %s because it depends"
+ + " on change %d which the on-behalf-of user does not have READ permission for",
+ triggeringChangeId.get(),
+ caller.getRealUser().getLoggableName(),
+ caller.getLoggableName(),
+ cd.getId().get());
+ } else {
+ logger.atFine().log(
+ "Change %d cannot be submitted by user %s on behalf of user %s because the"
+ + " on-behalf-of user does not have READ permission",
+ triggeringChangeId.get(),
+ caller.getRealUser().getLoggableName(),
+ caller.getLoggableName());
+ }
problems.add(
ChangeProblem.create(
cd.getId(),
@@ -514,13 +538,22 @@
return;
}
if (!can.contains(ChangePermission.SUBMIT_AS)) {
- logger.atFine().log(
- "Change %d cannot be submitted by user %s on behalf of user %s because it depends on"
- + " change %d which the user does not have SUBMIT_AS permission for",
- triggeringChangeId.get(),
- caller.getRealUser().getLoggableName(),
- caller.getLoggableName(),
- cd.getId().get());
+ if (triggeringChangeId.get() != cd.getId().get()) {
+ logger.atFine().log(
+ "Change %d cannot be submitted by user %s on behalf of user %s because it depends"
+ + " on change %d which the user does not have SUBMIT_AS permission for",
+ triggeringChangeId.get(),
+ caller.getRealUser().getLoggableName(),
+ caller.getLoggableName(),
+ cd.getId().get());
+ } else {
+ logger.atFine().log(
+ "Change %d cannot be submitted by user %s on behalf of user %s because they do not"
+ + " have SUBMIT_AS permission",
+ triggeringChangeId.get(),
+ caller.getRealUser().getLoggableName(),
+ caller.getLoggableName());
+ }
problems.add(
ChangeProblem.create(
cd.getId(),
diff --git