blob: e9832fdb701c46e427d58546e63e578390d8e57e [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.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 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)
&& 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 Code-Review approvals of the real uploader (aka the rebaser).
//
// 3. 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.
return !cd.currentPatchSet().uploader().equals(cd.currentPatchSet().realUploader());
}
private boolean hasCodeReviewApprovalOfRealUploader(ChangeData cd) {
return cd.currentApprovals().stream()
.anyMatch(psa -> psa.accountId().equals(cd.currentPatchSet().realUploader()));
}
private boolean ignoresCodeReviewApprovalsOfUploader(ChangeData cd) {
for (SubmitRequirement submitRequirement : cd.submitRequirements().keySet()) {
try {
Predicate<ChangeData> predicate =
submitRequirementChangequeryBuilder
.get()
.parse(submitRequirement.submittabilityExpression().expressionString());
return ignoresCodeReviewApprovalsOfUploader(predicate);
} catch (QueryParseException e) {
return false;
}
}
return false;
}
private boolean ignoresCodeReviewApprovalsOfUploader(Predicate<ChangeData> predicate) {
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;
}
}