| // Copyright (C) 2013 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.its.base.its; |
| |
| import static java.util.stream.Collectors.toList; |
| |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.common.data.AccessSection; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.Project.NameKey; |
| import com.google.gerrit.extensions.annotations.PluginName; |
| import com.google.gerrit.extensions.api.projects.CommentLinkInfo; |
| import com.google.gerrit.server.config.GerritInstanceId; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.gerrit.server.config.PluginConfig; |
| import com.google.gerrit.server.config.PluginConfigFactory; |
| import com.google.gerrit.server.events.ChangeAbandonedEvent; |
| import com.google.gerrit.server.events.ChangeMergedEvent; |
| import com.google.gerrit.server.events.ChangeRestoredEvent; |
| import com.google.gerrit.server.events.CommentAddedEvent; |
| import com.google.gerrit.server.events.PatchSetCreatedEvent; |
| import com.google.gerrit.server.events.PrivateStateChangedEvent; |
| import com.google.gerrit.server.events.RefEvent; |
| import com.google.gerrit.server.events.RefUpdatedEvent; |
| import com.google.gerrit.server.events.WorkInProgressStateChangedEvent; |
| import com.google.gerrit.server.project.NoSuchProjectException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.project.ProjectState; |
| import com.google.gerrit.server.project.RefPatternMatcher; |
| import com.google.inject.Inject; |
| import com.googlesource.gerrit.plugins.its.base.validation.ItsAssociationPolicy; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.regex.Pattern; |
| import org.eclipse.jgit.lib.Config; |
| |
| public class ItsConfig { |
| private static final String PLUGIN = "plugin"; |
| |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); |
| |
| private final String pluginName; |
| private final ProjectCache projectCache; |
| private final PluginConfigFactory pluginCfgFactory; |
| private final Config gerritConfig; |
| private String instanceId; |
| |
| private static final ThreadLocal<Project.NameKey> currentProjectName = |
| ThreadLocal.withInitial(() -> null); |
| |
| public static void setCurrentProjectName(Project.NameKey projectName) { |
| currentProjectName.set(projectName); |
| } |
| |
| @Inject |
| public ItsConfig( |
| @PluginName String pluginName, |
| ProjectCache projectCache, |
| PluginConfigFactory pluginCfgFactory, |
| @GerritServerConfig Config gerritConfig, |
| @Nullable @GerritInstanceId String instanceId) { |
| this.pluginName = pluginName; |
| this.projectCache = projectCache; |
| this.pluginCfgFactory = pluginCfgFactory; |
| this.gerritConfig = gerritConfig; |
| this.instanceId = instanceId; |
| } |
| |
| // Plugin enablement -------------------------------------------------------- |
| |
| public boolean isEnabled(RefEvent event) { |
| if ((instanceId == null && event.instanceId != null) |
| || (instanceId != null && !instanceId.equals(event.instanceId))) { |
| logger.atFine().log( |
| "Event %s is coming from a remote Gerrit instance-id (%s)", event, event.instanceId); |
| return false; |
| } |
| |
| if (event instanceof PatchSetCreatedEvent |
| || event instanceof CommentAddedEvent |
| || event instanceof ChangeMergedEvent |
| || event instanceof ChangeAbandonedEvent |
| || event instanceof ChangeRestoredEvent |
| || event instanceof PrivateStateChangedEvent |
| || event instanceof WorkInProgressStateChangedEvent |
| || event instanceof RefUpdatedEvent) { |
| return isEnabled(event.getProjectNameKey(), event.getRefName()); |
| } |
| logger.atFine().log("Event %s not recognised and ignored", event); |
| return false; |
| } |
| |
| public boolean isEnabled(Project.NameKey projectNK, String refName) { |
| Optional<ProjectState> projectState = projectCache.get(projectNK); |
| if (!projectState.isPresent()) { |
| logger.atSevere().log( |
| "Failed to check if %s is enabled for project %s: Project not found", |
| pluginName, projectNK.get()); |
| return false; |
| } |
| return isEnforcedByAnyParentProject(refName, projectState.get()) |
| || (isEnabledForProject(projectState.get()) |
| && isEnabledForBranch(projectState.get(), refName)); |
| } |
| |
| private boolean isEnforcedByAnyParentProject(String refName, ProjectState projectState) { |
| for (ProjectState parentState : projectState.treeInOrder()) { |
| PluginConfig parentCfg = pluginCfgFactory.getFromProjectConfig(parentState, pluginName); |
| if ("enforced".equals(parentCfg.getString("enabled", "false")) |
| && isEnabledForBranch(parentState, refName)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isEnabledForProject(ProjectState projectState) { |
| return !"false" |
| .equals( |
| pluginCfgFactory |
| .getFromProjectConfigWithInheritance(projectState, pluginName) |
| .getString("enabled", "false")); |
| } |
| |
| private boolean isEnabledForBranch(ProjectState project, String refName) { |
| String[] refPatterns = |
| pluginCfgFactory |
| .getFromProjectConfigWithInheritance(project, pluginName) |
| .getStringList("branch"); |
| if (refPatterns.length == 0) { |
| return true; |
| } |
| for (String refPattern : refPatterns) { |
| if (AccessSection.isValidRefSectionName(refPattern) && match(refName, refPattern)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean match(String refName, String refPattern) { |
| return RefPatternMatcher.getMatcher(refPattern).match(refName, null); |
| } |
| |
| // Project association |
| public Optional<String> getItsProjectName(Project.NameKey projectNK) { |
| Optional<ProjectState> projectState = projectCache.get(projectNK); |
| if (!projectState.isPresent()) { |
| return Optional.empty(); |
| } |
| return Optional.ofNullable( |
| pluginCfgFactory |
| .getFromProjectConfig(projectState.get(), pluginName) |
| .getString("its-project")); |
| } |
| |
| // Issue association -------------------------------------------------------- |
| |
| /** |
| * Gets the name of the comment link that should be used |
| * |
| * @return name of the comment link that should be used |
| */ |
| public String getCommentLinkName() { |
| String ret; |
| |
| ret = getPluginConfigString("commentlink"); |
| if (ret == null) { |
| ret = pluginName; |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * Gets the regular expression used to identify issue ids. |
| * |
| * <p>The index of the group that holds the issue id is {@link #getIssuePatternGroupIndex()}. |
| * |
| * @return the regular expression, or {@code null}, if there is no pattern to match issue ids. |
| */ |
| public Pattern getIssuePattern() { |
| Optional<String> match = |
| getCommentLinkInfo(getCommentLinkName()).stream() |
| .filter(input -> input.match != null && !input.match.trim().isEmpty()) |
| .map(input -> input.match) |
| .reduce((a, b) -> b); |
| |
| String defPattern = gerritConfig.getString("commentlink", getCommentLinkName(), "match"); |
| |
| if (!match.isPresent() && defPattern == null) { |
| return null; |
| } |
| |
| return Pattern.compile(match.orElse(defPattern)); |
| } |
| |
| /** |
| * Gets the index of the group in the issue pattern that holds the issue id. |
| * |
| * <p>The corresponding issue pattern is {@link #getIssuePattern()} |
| * |
| * @return the group index for {@link #getIssuePattern()} that holds the issue id. The group index |
| * is guaranteed to be a valid group index. |
| */ |
| public int getIssuePatternGroupIndex() { |
| Pattern pattern = getIssuePattern(); |
| int groupCount = pattern.matcher("").groupCount(); |
| int index = getPluginConfigInt("commentlinkGroupIndex", 1); |
| if (index < 0 || index > groupCount) { |
| index = (groupCount == 0 ? 0 : 1); |
| } |
| return index; |
| } |
| |
| /** |
| * Pattern to skip the mandatory check for an issue. Can be used to explicitly bypass the |
| * mandatory issue pattern check for some commits. |
| * |
| * <p>When no pattern is specified, it will return a pattern which never matches. |
| */ |
| public Optional<Pattern> getDummyIssuePattern() { |
| return Optional.ofNullable(getPluginConfigString("dummyIssuePattern")).map(Pattern::compile); |
| } |
| |
| /** |
| * Gets how necessary it is to associate commits with issues |
| * |
| * @return policy on how necessary association with issues is |
| */ |
| public ItsAssociationPolicy getItsAssociationPolicy() { |
| ItsAssociationPolicy legacyItsAssociationPolicy = |
| gerritConfig.getEnum( |
| "commentlink", getCommentLinkName(), "association", ItsAssociationPolicy.OPTIONAL); |
| |
| return getPluginConfigEnum("association", legacyItsAssociationPolicy); |
| } |
| |
| private String getPluginConfigString(String key) { |
| return getCurrentPluginConfig().getString(key, gerritConfig.getString(PLUGIN, pluginName, key)); |
| } |
| |
| private int getPluginConfigInt(String key, int defaultValue) { |
| return getCurrentPluginConfig() |
| .getInt(key, gerritConfig.getInt(PLUGIN, pluginName, key, defaultValue)); |
| } |
| |
| private <T extends Enum<?>> T getPluginConfigEnum(String key, T defaultValue) { |
| return getCurrentPluginConfig() |
| .getEnum(key, gerritConfig.getEnum(PLUGIN, pluginName, key, defaultValue)); |
| } |
| |
| private PluginConfig getCurrentPluginConfig() { |
| NameKey projectName = currentProjectName.get(); |
| if (projectName != null) { |
| try { |
| return pluginCfgFactory.getFromProjectConfigWithInheritance(projectName, pluginName); |
| } catch (NoSuchProjectException e) { |
| logger.atSevere().withCause(e).log( |
| "Cannot access %s configuration for plugin %s", projectName, pluginName); |
| } |
| } |
| return new PluginConfig(pluginName, new Config()); |
| } |
| |
| private List<CommentLinkInfo> getCommentLinkInfo(final String commentlinkName) { |
| NameKey projectName = currentProjectName.get(); |
| if (projectName != null) { |
| List<CommentLinkInfo> commentlinks = projectCache.get(projectName).get().getCommentLinks(); |
| return commentlinks.stream() |
| .filter(input -> input.name.equals(commentlinkName)) |
| .collect(toList()); |
| } |
| return Collections.emptyList(); |
| } |
| } |