| // Copyright (C) 2023 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.submit; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.gerrit.entities.SubmitRequirement; | |
| import com.google.gerrit.index.query.Predicate; | |
| import com.google.gerrit.index.query.QueryParseException; | |
| import com.google.gerrit.metrics.Counter0; | |
| import com.google.gerrit.metrics.Description; | |
| import com.google.gerrit.metrics.MetricMaker; | |
| import com.google.gerrit.server.query.change.ChangeData; | |
| import com.google.gerrit.server.query.change.MagicLabelPredicates; | |
| import com.google.gerrit.server.query.change.SubmitRequirementChangeQueryBuilder; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Provider; | |
| /** Metrics are recorded when a change is merged (aka submitted). */ | |
| public class MergeMetrics { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| private final Provider<SubmitRequirementChangeQueryBuilder> submitRequirementChangequeryBuilder; | |
| // TODO: This metric is for measuring the impact of allowing users to rebase changes on behalf of | |
| // the uploader. Once this feature has been rolled out and its impact as been measured, we may | |
| // remove this metric. | |
| private final Counter0 countChangesThatWereSubmittedWithRebaserApproval; | |
| @Inject | |
| public MergeMetrics( | |
| Provider<SubmitRequirementChangeQueryBuilder> submitRequirementChangequeryBuilder, | |
| MetricMaker metricMaker) { | |
| this.submitRequirementChangequeryBuilder = submitRequirementChangequeryBuilder; | |
| this.countChangesThatWereSubmittedWithRebaserApproval = | |
| metricMaker.newCounter( | |
| "change/submitted_with_rebaser_approval", | |
| new Description( | |
| "Number of rebased changes that were submitted with a Code-Review approval of" | |
| + " the rebaser that would not have been submittable if the rebase was not" | |
| + " done on behalf of the uploader.") | |
| .setRate()); | |
| } | |
| public void countChangesThatWereSubmittedWithRebaserApproval(ChangeData cd) { | |
| if (isRebaseOnBehalfOfUploader(cd) | |
| && hasCodeReviewApprovalOfRealUploader(cd) | |
| && !hasCodeReviewApprovalOfUserThatIsNotTheRealUploader(cd) | |
| && ignoresCodeReviewApprovalsOfUploader(cd)) { | |
| // 1. The patch set that is being submitted was created by rebasing on behalf of the uploader. | |
| // The uploader of the patch set is the original uploader on whom's behalf the rebase was | |
| // done. The real uploader is the user that did the rebase on behalf of the uploader (e.g. by | |
| // clicking on the rebase button). | |
| // | |
| // 2. The change has a Code-Review approval of the real uploader (aka the rebaser). | |
| // | |
| // 3. The change doesn't have a Code-Review approval of any other user (a user that is not the | |
| // real uploader). | |
| // | |
| // 4. Code-Review approvals of the uploader are ignored. | |
| // | |
| // If instead of a rebase on behalf of the uploader a normal rebase would have been done the | |
| // rebaser would have been the uploader of the patch set. In this case the Code-Review | |
| // approval of the rebaser would not have counted since Code-Review approvals of the uploader | |
| // are ignored. | |
| // | |
| // In this case we assume that the change would not be submittable if a normal rebase had been | |
| // done. This is not always correct (e.g. if there are approvals of multiple reviewers) but | |
| // it's good enough for the metric. | |
| countChangesThatWereSubmittedWithRebaserApproval.increment(); | |
| } | |
| } | |
| private boolean isRebaseOnBehalfOfUploader(ChangeData cd) { | |
| // If the uploader differs from the real uploader the upload of the patch set has been | |
| // impersonated. Impersonating the uploader is only allowed on rebase by rebasing on behalf of | |
| // the uploader. Hence if the current patch set has different accounts as uploader and real | |
| // uploader we can assume that it was created by rebase on behalf of the uploader. | |
| boolean isRebaseOnBehalfOfUploader = | |
| !cd.currentPatchSet().uploader().equals(cd.currentPatchSet().realUploader()); | |
| logger.atFine().log("isRebaseOnBehalfOfUploader = %s", isRebaseOnBehalfOfUploader); | |
| return isRebaseOnBehalfOfUploader; | |
| } | |
| private boolean hasCodeReviewApprovalOfRealUploader(ChangeData cd) { | |
| boolean hasCodeReviewApprovalOfRealUploader = | |
| cd.currentApprovals().stream() | |
| .anyMatch(psa -> psa.accountId().equals(cd.currentPatchSet().realUploader())); | |
| logger.atFine().log( | |
| "hasCodeReviewApprovalOfRealUploader = %s", hasCodeReviewApprovalOfRealUploader); | |
| return hasCodeReviewApprovalOfRealUploader; | |
| } | |
| private boolean hasCodeReviewApprovalOfUserThatIsNotTheRealUploader(ChangeData cd) { | |
| boolean hasCodeReviewApprovalOfUserThatIsNotTheRealUploader = | |
| cd.currentApprovals().stream() | |
| .anyMatch(psa -> !psa.accountId().equals(cd.currentPatchSet().realUploader())); | |
| logger.atFine().log( | |
| "hasCodeReviewApprovalOfUserThatIsNotTheRealUploader = %s", | |
| hasCodeReviewApprovalOfUserThatIsNotTheRealUploader); | |
| return hasCodeReviewApprovalOfUserThatIsNotTheRealUploader; | |
| } | |
| private boolean ignoresCodeReviewApprovalsOfUploader(ChangeData cd) { | |
| for (SubmitRequirement submitRequirement : cd.submitRequirements().keySet()) { | |
| try { | |
| Predicate<ChangeData> predicate = | |
| submitRequirementChangequeryBuilder | |
| .get() | |
| .parse(submitRequirement.submittabilityExpression().expressionString()); | |
| boolean ignoresCodeReviewApprovalsOfUploader = | |
| ignoresCodeReviewApprovalsOfUploader(predicate); | |
| logger.atFine().log( | |
| "ignoresCodeReviewApprovalsOfUploader = %s", ignoresCodeReviewApprovalsOfUploader); | |
| if (ignoresCodeReviewApprovalsOfUploader) { | |
| return true; | |
| } | |
| } catch (QueryParseException e) { | |
| logger.atFine().log( | |
| "Failed to parse submit requirement expression %s: %s", | |
| submitRequirement.submittabilityExpression().expressionString(), e.getMessage()); | |
| // ignore and inspect the next submit requirement | |
| } | |
| } | |
| return false; | |
| } | |
| private boolean ignoresCodeReviewApprovalsOfUploader(Predicate<ChangeData> predicate) { | |
| logger.atFine().log( | |
| "predicate = (%s) %s (child count = %d)", | |
| predicate.getClass().getName(), predicate, predicate.getChildCount()); | |
| if (predicate.getChildCount() == 0) { | |
| // Submit requirements may require a Code-Review approval but ignore approvals by the | |
| // uploader. This is done by using a label predicate with 'user=non_uploader' or | |
| // 'user=non_contributor', e.g. 'label:Code-Review=+2,user=non_uploader'. After the submit | |
| // requirement expression has been parsed these label predicates are represented by | |
| // MagicLabelPredicate in the predicate tree. Hence to know whether Code-Review approvals of | |
| // the uploader are ignored, we must check if there is any MagicLabelPredicate for the | |
| // Code-Review label that ignores approvals of the uploader (aka has user set to non_uploader | |
| // or non_contributor). | |
| if (predicate instanceof MagicLabelPredicates.PostFilterMagicLabelPredicate) { | |
| MagicLabelPredicates.PostFilterMagicLabelPredicate magicLabelPredicate = | |
| (MagicLabelPredicates.PostFilterMagicLabelPredicate) predicate; | |
| if (magicLabelPredicate.getLabel().equalsIgnoreCase("Code-Review") | |
| && magicLabelPredicate.ignoresUploaderApprovals()) { | |
| return true; | |
| } | |
| } else if (predicate instanceof MagicLabelPredicates.IndexMagicLabelPredicate) { | |
| MagicLabelPredicates.IndexMagicLabelPredicate magicLabelPredicate = | |
| (MagicLabelPredicates.IndexMagicLabelPredicate) predicate; | |
| if (magicLabelPredicate.getLabel().equalsIgnoreCase("Code-Review") | |
| && magicLabelPredicate.ignoresUploaderApprovals()) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| for (Predicate<ChangeData> childPredicate : predicate.getChildren()) { | |
| if (ignoresCodeReviewApprovalsOfUploader(childPredicate)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| } |