blob: b56d9ef8eb630732b880208c5d34d116bc9e0259 [file] [log] [blame]
// 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;
}
}