All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.ReviewerRecommender Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc4
Show newest version
// Copyright (C) 2016 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 static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
import static java.util.stream.Collectors.toList;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountDirectory.FillOptions;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.change.ReviewerSuggestion;
import com.google.gerrit.server.change.SuggestReviewers;
import com.google.gerrit.server.change.SuggestedReviewer;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.apache.commons.lang.mutable.MutableDouble;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ReviewerRecommender {
  private static final Logger log = LoggerFactory.getLogger(ReviewerRecommender.class);
  private static final double BASE_REVIEWER_WEIGHT = 10;
  private static final double BASE_OWNER_WEIGHT = 1;
  private static final double BASE_COMMENT_WEIGHT = 0.5;
  private static final double[] WEIGHTS =
      new double[] {
        BASE_REVIEWER_WEIGHT, BASE_OWNER_WEIGHT, BASE_COMMENT_WEIGHT,
      };
  private static final long PLUGIN_QUERY_TIMEOUT = 500; // ms

  private final ChangeQueryBuilder changeQueryBuilder;
  private final Config config;
  private final DynamicMap reviewerSuggestionPluginMap;
  private final InternalChangeQuery internalChangeQuery;
  private final WorkQueue workQueue;
  private final Provider dbProvider;
  private final ApprovalsUtil approvalsUtil;

  @Inject
  ReviewerRecommender(
      ChangeQueryBuilder changeQueryBuilder,
      DynamicMap reviewerSuggestionPluginMap,
      InternalChangeQuery internalChangeQuery,
      WorkQueue workQueue,
      Provider dbProvider,
      ApprovalsUtil approvalsUtil,
      @GerritServerConfig Config config) {
    Set fillOptions = EnumSet.of(FillOptions.SECONDARY_EMAILS);
    fillOptions.addAll(AccountLoader.DETAILED_OPTIONS);
    this.changeQueryBuilder = changeQueryBuilder;
    this.config = config;
    this.internalChangeQuery = internalChangeQuery;
    this.reviewerSuggestionPluginMap = reviewerSuggestionPluginMap;
    this.workQueue = workQueue;
    this.dbProvider = dbProvider;
    this.approvalsUtil = approvalsUtil;
  }

  public List suggestReviewers(
      ChangeNotes changeNotes,
      SuggestReviewers suggestReviewers,
      ProjectControl projectControl,
      List candidateList)
      throws OrmException {
    String query = suggestReviewers.getQuery();
    double baseWeight = config.getInt("addReviewer", "baseWeight", 1);

    Map reviewerScores;
    if (Strings.isNullOrEmpty(query)) {
      reviewerScores = baseRankingForEmptyQuery(baseWeight);
    } else {
      reviewerScores = baseRankingForCandidateList(candidateList, projectControl, baseWeight);
    }

    // Send the query along with a candidate list to all plugins and merge the
    // results. Plugins don't necessarily need to use the candidates list, they
    // can also return non-candidate account ids.
    List>> tasks =
        new ArrayList<>(reviewerSuggestionPluginMap.plugins().size());
    List weights = new ArrayList<>(reviewerSuggestionPluginMap.plugins().size());

    for (DynamicMap.Entry plugin : reviewerSuggestionPluginMap) {
      tasks.add(
          () ->
              plugin
                  .getProvider()
                  .get()
                  .suggestReviewers(
                      projectControl.getProject().getNameKey(),
                      changeNotes != null ? changeNotes.getChangeId() : null,
                      query,
                      reviewerScores.keySet()));
      String key = plugin.getPluginName() + "-" + plugin.getExportName();
      String pluginWeight = config.getString("addReviewer", key, "weight");
      if (Strings.isNullOrEmpty(pluginWeight)) {
        pluginWeight = "1";
      }
      log.debug("weight for {}: {}", key, pluginWeight);
      try {
        weights.add(Double.parseDouble(pluginWeight));
      } catch (NumberFormatException e) {
        log.error("Exception while parsing weight for {}", key, e);
        weights.add(1d);
      }
    }

    try {
      List>> futures =
          workQueue.getDefaultQueue().invokeAll(tasks, PLUGIN_QUERY_TIMEOUT, TimeUnit.MILLISECONDS);
      Iterator weightIterator = weights.iterator();
      for (Future> f : futures) {
        double weight = weightIterator.next();
        for (SuggestedReviewer s : f.get()) {
          if (reviewerScores.containsKey(s.account)) {
            reviewerScores.get(s.account).add(s.score * weight);
          } else {
            reviewerScores.put(s.account, new MutableDouble(s.score * weight));
          }
        }
      }
    } catch (ExecutionException | InterruptedException e) {
      log.error("Exception while suggesting reviewers", e);
      return ImmutableList.of();
    }

    if (changeNotes != null) {
      // Remove change owner
      reviewerScores.remove(changeNotes.getChange().getOwner());

      // Remove existing reviewers
      reviewerScores
          .keySet()
          .removeAll(approvalsUtil.getReviewers(dbProvider.get(), changeNotes).byState(REVIEWER));
    }

    // Sort results
    Stream> sorted =
        reviewerScores
            .entrySet()
            .stream()
            .sorted(Collections.reverseOrder(Map.Entry.comparingByValue()));
    List sortedSuggestions = sorted.map(Map.Entry::getKey).collect(toList());
    return sortedSuggestions;
  }

  private Map baseRankingForEmptyQuery(double baseWeight)
      throws OrmException {
    // Get the user's last 25 changes, check approvals
    try {
      List result =
          internalChangeQuery
              .setLimit(25)
              .setRequestedFields(ImmutableSet.of(ChangeField.REVIEWER.getName()))
              .query(changeQueryBuilder.owner("self"));
      Map suggestions = new HashMap<>();
      for (ChangeData cd : result) {
        for (PatchSetApproval approval : cd.currentApprovals()) {
          Account.Id id = approval.getAccountId();
          if (suggestions.containsKey(id)) {
            suggestions.get(id).add(baseWeight);
          } else {
            suggestions.put(id, new MutableDouble(baseWeight));
          }
        }
      }
      return suggestions;
    } catch (QueryParseException e) {
      // Unhandled, because owner:self will never provoke a QueryParseException
      log.error("Exception while suggesting reviewers", e);
      return ImmutableMap.of();
    }
  }

  private Map baseRankingForCandidateList(
      List candidates, ProjectControl projectControl, double baseWeight)
      throws OrmException {
    // Get each reviewer's activity based on number of applied labels
    // (weighted 10d), number of comments (weighted 0.5d) and number of owned
    // changes (weighted 1d).
    Map reviewers = new LinkedHashMap<>();
    if (candidates.size() == 0) {
      return reviewers;
    }
    List> predicates = new ArrayList<>();
    for (Account.Id id : candidates) {
      try {
        Predicate projectQuery =
            changeQueryBuilder.project(projectControl.getProject().getName());

        // Get all labels for this project and create a compound OR query to
        // fetch all changes where users have applied one of these labels
        List labelTypes = projectControl.getLabelTypes().getLabelTypes();
        List> labelPredicates = new ArrayList<>(labelTypes.size());
        for (LabelType type : labelTypes) {
          labelPredicates.add(changeQueryBuilder.label(type.getName() + ",user=" + id));
        }
        Predicate reviewerQuery =
            Predicate.and(projectQuery, Predicate.or(labelPredicates));

        Predicate ownerQuery =
            Predicate.and(projectQuery, changeQueryBuilder.owner(id.toString()));
        Predicate commentedByQuery =
            Predicate.and(projectQuery, changeQueryBuilder.commentby(id.toString()));

        predicates.add(reviewerQuery);
        predicates.add(ownerQuery);
        predicates.add(commentedByQuery);
        reviewers.put(id, new MutableDouble());
      } catch (QueryParseException e) {
        // Unhandled: If an exception is thrown, we won't increase the
        // candidates's score
        log.error("Exception while suggesting reviewers", e);
      }
    }

    List> result =
        internalChangeQuery.setLimit(25).setRequestedFields(ImmutableSet.of()).query(predicates);

    Iterator> queryResultIterator = result.iterator();
    Iterator reviewersIterator = reviewers.keySet().iterator();

    int i = 0;
    Account.Id currentId = null;
    while (queryResultIterator.hasNext()) {
      List currentResult = queryResultIterator.next();
      if (i % WEIGHTS.length == 0) {
        currentId = reviewersIterator.next();
      }

      reviewers.get(currentId).add(WEIGHTS[i % WEIGHTS.length] * baseWeight * currentResult.size());
      i++;
    }
    return reviewers;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy