blob: f99b3fe4550f17fa853e8722784f36164bb6477b [file] [log] [blame]
// 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.googlesource.gerrit.plugins.automerger;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.re2j.Pattern;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Class to read the config. */
@Singleton
public class ConfigLoader {
private static final Logger log = LoggerFactory.getLogger(ConfigLoader.class);
private static final String BRANCH_DELIMITER = ":";
private static final String DEFAULT_CONFLICT_MESSAGE = "Merge conflict found on ${branch}";
private final GerritApi gApi;
private final String pluginName;
private final String canonicalWebUrl;
private final AllProjectsName allProjectsName;
private final PluginConfigFactory cfgFactory;
private final Provider<CurrentUser> user;
/**
* Class to handle getting information from the config.
*
* @param gApi API to access gerrit information.
* @param allProjectsName The name of the top-level project.
* @param pluginName The name of the plugin we are running.
* @param cfgFactory Factory to generate the plugin config.
*/
@Inject
public ConfigLoader(
GerritApi gApi,
AllProjectsName allProjectsName,
@PluginName String pluginName,
@CanonicalWebUrl String canonicalWebUrl,
PluginConfigFactory cfgFactory,
Provider<CurrentUser> user) {
this.gApi = gApi;
this.canonicalWebUrl = canonicalWebUrl;
this.pluginName = pluginName;
this.cfgFactory = cfgFactory;
this.allProjectsName = allProjectsName;
this.user = user;
}
private Config getConfig() throws ConfigInvalidException {
try {
return cfgFactory.getProjectPluginConfig(allProjectsName, pluginName);
} catch (NoSuchProjectException e) {
throw new ConfigInvalidException(
"Config invalid because " + allProjectsName.get() + " does not exist!");
}
}
/**
* Detects whether to skip a change based on the configuration.
*
* @param fromBranch Branch we are merging from.
* @param toBranch Branch we are merging to.
* @param commitMessage Commit message of the change.
* @return True if we match blank_merge_regex and merge_all is false, or we match
* always_blank_merge_regex
* @throws ConfigInvalidException
*/
public boolean isSkipMerge(String fromBranch, String toBranch, String commitMessage)
throws ConfigInvalidException {
Pattern alwaysBlankMergePattern = getConfigPattern("alwaysBlankMerge");
if (alwaysBlankMergePattern.matches(commitMessage)) {
return true;
}
Pattern blankMergePattern = getConfigPattern("blankMerge");
// If regex matches blank_merge (DO NOT MERGE), skip iff merge_all is false
if (blankMergePattern.matches(commitMessage)) {
return !getMergeAll(fromBranch, toBranch);
}
return false;
}
private Pattern getConfigPattern(String key) throws ConfigInvalidException {
String[] patternList = getConfig().getStringList("global", null, key);
Set<String> mergeStrings = new HashSet<>(Arrays.asList(patternList));
return Pattern.compile(Joiner.on("|").join(mergeStrings), Pattern.DOTALL);
}
private boolean getMergeAll(String fromBranch, String toBranch) throws ConfigInvalidException {
return getConfig()
.getBoolean("automerger", fromBranch + BRANCH_DELIMITER + toBranch, "mergeAll", false);
}
/**
* Returns the name of the automerge label (i.e. the label to vote -1 if we have a merge conflict)
*
* @return Returns the name of the automerge label.
* @throws ConfigInvalidException
*/
public String getAutomergeLabel() throws ConfigInvalidException {
String automergeLabel = getConfig().getString("global", null, "automergeLabel");
return automergeLabel != null ? automergeLabel : "Code-Review";
}
/**
* Returns the hostName.
*
* <p>Uses the hostName defined in the configuration if specified. If not, defaults to the
* canonicalWebUrl.
*
* @return Returns the hostname
* @throws ConfigInvalidException
*/
public String getHostName() throws ConfigInvalidException {
String hostName = getConfig().getString("global", null, "hostName");
return hostName != null ? hostName : canonicalWebUrl;
}
/**
* Returns a string to append to the end of the merge conflict message.
*
* @return The message string, or the empty string if nothing is specified.
* @throws ConfigInvalidException
*/
public String getConflictMessage() throws ConfigInvalidException {
String conflictMessage = getConfig().getString("global", null, "conflictMessage");
if (Strings.isNullOrEmpty(conflictMessage)) {
conflictMessage = DEFAULT_CONFLICT_MESSAGE;
}
return conflictMessage;
}
/**
* Returns a string to append to the end of the merge conflict message for the manifest project.
*
* @return The message string, or the empty string if nothing is specified.
* @throws ConfigInvalidException
*/
public String getManifestConflictMessage() throws ConfigInvalidException {
String conflictMessage = getConfig().getString("global", null, "manifestConflictMessage");
if (Strings.isNullOrEmpty(conflictMessage)) {
conflictMessage = getConflictMessage();
}
return conflictMessage;
}
/**
* Get the projects that should be merged for the given pair of branches.
*
* @param fromBranch Branch we are merging from.
* @param toBranch Branch we are merging to.
* @return The projects that are in scope of the given projects.
* @throws RestApiException
* @throws IOException
* @throws ConfigInvalidException
*/
public Set<String> getProjectsInScope(String fromBranch, String toBranch)
throws RestApiException, IOException, ConfigInvalidException {
try {
Set<String> projectSet = getManifestProjects(fromBranch, toBranch);
projectSet = applyConfig(fromBranch, toBranch, projectSet);
log.debug("Project set for {} to {} is {}", fromBranch, toBranch, projectSet);
return projectSet;
} catch (RestApiException | IOException e) {
log.error("Error reading manifest for {}!", fromBranch, e);
throw e;
}
}
/**
* Gets the upstream branches of the given branch and project.
*
* @param toBranch The downstream branch we would merge to.
* @param project The project we are merging.
* @return The branches upstream of the given branch for the given project.
* @throws RestApiException
* @throws IOException
* @throws ConfigInvalidException
*/
public Set<String> getUpstreamBranches(String toBranch, String project)
throws ConfigInvalidException, RestApiException, IOException {
if (toBranch == null) {
throw new IllegalArgumentException("toBranch cannot be null");
}
Set<String> upstreamBranches = new HashSet<String>();
// List all subsections of automerger, split by :
Set<String> subsections = getConfig().getSubsections(pluginName);
for (String subsection : subsections) {
// Subsections are of the form "fromBranch:toBranch"
List<String> branchPair =
Splitter.on(BRANCH_DELIMITER).trimResults().omitEmptyStrings().splitToList(subsection);
if (branchPair.size() != 2) {
throw new ConfigInvalidException("Automerger config branch pair malformed: " + subsection);
}
if (toBranch.equals(branchPair.get(1))) {
// If toBranch matches, check if project is in both their manifests
Set<String> projectsInScope = getProjectsInScope(branchPair.get(0), branchPair.get(1));
if (projectsInScope.contains(project)) {
upstreamBranches.add(branchPair.get(0));
}
}
}
return upstreamBranches;
}
/**
* Gets the downstream branches of the given branch and project.
*
* @param fromBranch The branch we are merging from.
* @param project The project we are merging.
* @return The branches downstream of the given branch for the given project.
* @throws RestApiException
* @throws IOException
* @throws ConfigInvalidException
*/
public Set<String> getDownstreamBranches(String fromBranch, String project)
throws RestApiException, IOException, ConfigInvalidException {
Set<String> downstreamBranches = new HashSet<String>();
// List all subsections of automerger, split by :
Set<String> subsections = getConfig().getSubsections(pluginName);
for (String subsection : subsections) {
// Subsections are of the form "fromBranch:toBranch"
String[] branchPair = subsection.split(Pattern.quote(BRANCH_DELIMITER));
if (branchPair.length != 2) {
throw new ConfigInvalidException("Automerger config branch pair malformed: " + subsection);
}
if (fromBranch.equals(branchPair[0])) {
// If fromBranches match, check if project is in both their manifests
Set<String> projectsInScope = getProjectsInScope(branchPair[0], branchPair[1]);
if (projectsInScope.contains(project)) {
downstreamBranches.add(branchPair[1]);
}
}
}
return downstreamBranches;
}
public Set<String> getAllDownstreamBranches(String branch, String project)
throws RestApiException, IOException, ConfigInvalidException {
Set<String> downstreamBranches = new HashSet<>();
Set<String> immediateDownstreams = getDownstreamBranches(branch, project);
downstreamBranches.addAll(immediateDownstreams);
for (String immediateDownstream : immediateDownstreams) {
downstreamBranches.addAll(getAllDownstreamBranches(immediateDownstream, project));
}
return downstreamBranches;
}
public String getMissingDownstreamsMessage() throws ConfigInvalidException {
String message = getConfig().getString("global", null, "missingDownstreamsMessage");
if (message == null) {
message =
"Missing downstream branches ${missingDownstreams}. Please recreate the automerges. "
+ "If your topic contains quotes or braces, please remove them.";
}
return message;
}
public short getMinAutomergeVote() throws ConfigInvalidException {
return (short) getConfig().getInt("global", "minAutomergeVote", -2);
}
public boolean minAutomergeVoteDisabled() throws ConfigInvalidException {
return getConfig().getBoolean("global", "disableMinAutomergeVote", false);
}
public Account.Id getContextUserId() throws ConfigInvalidException {
int contextUserId = getConfig().getInt("global", "contextUserId", -1);
if (contextUserId > 0) {
return new Account.Id(contextUserId);
}
return user.get().getAccountId();
}
/**
* Returns overriden manifest config if specified, default if not
*
* @return The string name of the manifest project.
* @throws ConfigInvalidException
*/
public String getManifestProject() throws ConfigInvalidException {
String manifestProject = getConfig().getString("global", null, "manifestProject");
if (manifestProject == null) {
throw new ConfigInvalidException("manifestProject not specified.");
}
return manifestProject;
}
// Returns overriden manifest config if specified, default if not
private String getManifestFile() throws ConfigInvalidException {
String manifestFile = getConfig().getString("global", null, "manifestFile");
if (manifestFile == null) {
throw new ConfigInvalidException("manifestFile not specified.");
}
return manifestFile;
}
// Returns contents of manifest file for the given branch pair
// If manifest does not exist, return empty set.
private Set<String> getManifestProjects(String fromBranch, String toBranch)
throws RestApiException, IOException, ConfigInvalidException {
boolean ignoreSourceManifest =
getConfig()
.getBoolean(
"automerger",
fromBranch + BRANCH_DELIMITER + toBranch,
"ignoreSourceManifest",
false);
Set<String> toProjects =
getProjectsInManifest(getManifestProject(), getManifestFile(), toBranch);
if (ignoreSourceManifest) {
return toProjects;
}
Set<String> fromProjects =
getProjectsInManifest(getManifestProject(), getManifestFile(), fromBranch);
fromProjects.retainAll(toProjects);
return fromProjects;
}
private Set<String> getProjectsInManifest(
String manifestProject, String manifestFile, String branch)
throws RestApiException, IOException {
try (BinaryResult manifestConfig =
gApi.projects().name(manifestProject).branch(branch).file(manifestFile)) {
ManifestReader manifestReader = new ManifestReader(branch, manifestConfig.asString());
return manifestReader.getProjects();
} catch (ResourceNotFoundException e) {
log.debug("Manifest for {} not found", branch);
return new HashSet<>();
}
}
private Set<String> applyConfig(String fromBranch, String toBranch, Set<String> inputProjects)
throws ConfigInvalidException {
Set<String> projects = new HashSet<>(inputProjects);
List<String> setProjects =
Arrays.asList(
getConfig()
.getStringList(
"automerger", fromBranch + BRANCH_DELIMITER + toBranch, "setProjects"));
if (!setProjects.isEmpty()) {
projects.clear();
projects.addAll(setProjects);
}
List<String> addProjects =
Arrays.asList(
getConfig()
.getStringList(
"automerger", fromBranch + BRANCH_DELIMITER + toBranch, "addProjects"));
projects.addAll(addProjects);
List<String> ignoreProjects =
Arrays.asList(
getConfig()
.getStringList(
"automerger", fromBranch + BRANCH_DELIMITER + toBranch, "ignoreProjects"));
projects.removeAll(ignoreProjects);
return projects;
}
}