| // Copyright (C) 2021 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.plugins.codeowners.backend; |
| |
| import static java.util.stream.Collectors.joining; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.ChangeMessage; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.extensions.common.AccountInfo; |
| import com.google.gerrit.extensions.events.ReviewerAddedListener; |
| import com.google.gerrit.extensions.restapi.RestApiException; |
| import com.google.gerrit.metrics.Timer0; |
| import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfigSnapshot; |
| import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; |
| import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics; |
| import com.google.gerrit.plugins.codeowners.util.JgitPath; |
| import com.google.gerrit.server.ChangeMessagesUtil; |
| import com.google.gerrit.server.CurrentUser; |
| import com.google.gerrit.server.account.AccountCache; |
| import com.google.gerrit.server.notedb.ChangeNotes; |
| import com.google.gerrit.server.update.BatchUpdate; |
| import com.google.gerrit.server.update.BatchUpdateOp; |
| import com.google.gerrit.server.update.ChangeContext; |
| import com.google.gerrit.server.update.RetryHelper; |
| import com.google.gerrit.server.util.time.TimeUtil; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.nio.file.Path; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.stream.Stream; |
| |
| /** |
| * Callback that is invoked when a user is added as a reviewer. |
| * |
| * <p>If a code owner was added as reviewer add a change message that lists the files that are owned |
| * by the reviewer. |
| */ |
| @Singleton |
| public class CodeOwnersOnAddReviewer implements ReviewerAddedListener { |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private static final String TAG_ADD_REVIEWER = |
| ChangeMessagesUtil.AUTOGENERATED_BY_GERRIT_TAG_PREFIX + "code-owners:addReviewer"; |
| |
| private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; |
| private final CodeOwnerApprovalCheck codeOwnerApprovalCheck; |
| private final Provider<CurrentUser> userProvider; |
| private final RetryHelper retryHelper; |
| private final ChangeNotes.Factory changeNotesFactory; |
| private final AccountCache accountCache; |
| private final ChangeMessagesUtil changeMessageUtil; |
| private final CodeOwnerMetrics codeOwnerMetrics; |
| |
| @Inject |
| CodeOwnersOnAddReviewer( |
| CodeOwnersPluginConfiguration codeOwnersPluginConfiguration, |
| CodeOwnerApprovalCheck codeOwnerApprovalCheck, |
| Provider<CurrentUser> userProvider, |
| RetryHelper retryHelper, |
| ChangeNotes.Factory changeNotesFactory, |
| AccountCache accountCache, |
| ChangeMessagesUtil changeMessageUtil, |
| CodeOwnerMetrics codeOwnerMetrics) { |
| this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; |
| this.codeOwnerApprovalCheck = codeOwnerApprovalCheck; |
| this.userProvider = userProvider; |
| this.retryHelper = retryHelper; |
| this.changeNotesFactory = changeNotesFactory; |
| this.accountCache = accountCache; |
| this.changeMessageUtil = changeMessageUtil; |
| this.codeOwnerMetrics = codeOwnerMetrics; |
| } |
| |
| @Override |
| public void onReviewersAdded(Event event) { |
| Change.Id changeId = Change.id(event.getChange()._number); |
| Project.NameKey projectName = Project.nameKey(event.getChange().project); |
| |
| CodeOwnersPluginConfigSnapshot codeOwnersConfig = |
| codeOwnersPluginConfiguration.getProjectConfig(projectName); |
| int maxPathsInChangeMessages = codeOwnersConfig.getMaxPathsInChangeMessages(); |
| if (codeOwnersConfig.isDisabled(event.getChange().branch) || maxPathsInChangeMessages <= 0) { |
| return; |
| } |
| |
| try (Timer0.Context ctx = codeOwnerMetrics.addChangeMessageOnAddReviewer.start()) { |
| retryHelper |
| .changeUpdate( |
| "addCodeOwnersMessageOnAddReviewer", |
| updateFactory -> { |
| try (BatchUpdate batchUpdate = |
| updateFactory.create(projectName, userProvider.get(), TimeUtil.nowTs())) { |
| batchUpdate.addOp( |
| changeId, new Op(event.getReviewers(), maxPathsInChangeMessages)); |
| batchUpdate.execute(); |
| } |
| return null; |
| }) |
| .call(); |
| } catch (Exception e) { |
| logger.atSevere().withCause(e).log( |
| String.format( |
| "Failed to post code-owners change message for reviewer on change %s in project %s.", |
| changeId, projectName)); |
| } |
| } |
| |
| private class Op implements BatchUpdateOp { |
| private final List<AccountInfo> reviewers; |
| private final int limit; |
| |
| Op(List<AccountInfo> reviewers, int limit) { |
| this.reviewers = reviewers; |
| this.limit = limit; |
| } |
| |
| @Override |
| public boolean updateChange(ChangeContext ctx) throws Exception { |
| String message = |
| reviewers.stream() |
| .map(accountInfo -> Account.id(accountInfo._accountId)) |
| .map( |
| reviewerAccountId -> |
| buildMessageForReviewer( |
| ctx.getProject(), ctx.getChange().getId(), reviewerAccountId)) |
| .filter(Optional::isPresent) |
| .map(Optional::get) |
| .collect(joining("\n")); |
| |
| if (message.isEmpty()) { |
| return false; |
| } |
| |
| ChangeMessage changeMessage = ChangeMessagesUtil.newMessage(ctx, message, TAG_ADD_REVIEWER); |
| changeMessageUtil.addChangeMessage( |
| ctx.getUpdate(ctx.getChange().currentPatchSetId()), changeMessage); |
| return true; |
| } |
| |
| private Optional<String> buildMessageForReviewer( |
| Project.NameKey projectName, Change.Id changeId, Account.Id reviewerAccountId) { |
| ChangeNotes changeNotes = changeNotesFactory.create(projectName, changeId); |
| |
| ImmutableList<Path> ownedPaths; |
| try { |
| // limit + 1, so that we can show an indicator if there are more than <limit> files. |
| ownedPaths = |
| codeOwnerApprovalCheck.getOwnedPaths( |
| changeNotes, |
| changeNotes.getCurrentPatchSet(), |
| reviewerAccountId, |
| /* start= */ 0, |
| limit + 1); |
| } catch (RestApiException e) { |
| logger.atFine().withCause(e).log( |
| "Couldn't compute owned paths of change %s for account %s", |
| changeNotes.getChangeId(), reviewerAccountId.get()); |
| return Optional.empty(); |
| } |
| |
| if (ownedPaths.isEmpty()) { |
| // this reviewer doesn't own any of the modified paths |
| return Optional.empty(); |
| } |
| |
| Account reviewerAccount = accountCache.getEvenIfMissing(reviewerAccountId).account(); |
| |
| StringBuilder message = new StringBuilder(); |
| message.append( |
| String.format( |
| "%s who was added as reviewer owns the following files:\n", |
| reviewerAccount.getName())); |
| |
| if (ownedPaths.size() <= limit) { |
| appendPaths(message, ownedPaths.stream()); |
| } else { |
| appendPaths(message, ownedPaths.stream().limit(limit)); |
| message.append("(more files)\n"); |
| } |
| |
| return Optional.of(message.toString()); |
| } |
| |
| private void appendPaths(StringBuilder message, Stream<Path> pathsToAppend) { |
| pathsToAppend.forEach( |
| path -> message.append(String.format("* %s\n", JgitPath.of(path).get()))); |
| } |
| } |
| } |