/* * The MIT License * * Copyright (c) 2018, CloudBees, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package jenkins.branch.buildstrategies.basic; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.Util; import hudson.model.AbstractDescribableImpl; import hudson.model.Descriptor; import hudson.model.TaskListener; import hudson.util.FormValidation; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import javax.annotation.Nonnull; import hudson.util.LogTaskListener; import jenkins.branch.BranchBuildStrategy; import jenkins.branch.BranchBuildStrategyDescriptor; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; import jenkins.scm.api.mixin.ChangeRequestSCMHead; import jenkins.scm.api.mixin.TagSCMHead; import org.apache.commons.lang.StringUtils; import org.jenkinsci.Symbol; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; /** * A {@link BranchBuildStrategy} that builds branches with specific names. * * @since 1.0.1 */ public class NamedBranchBuildStrategyImpl extends BranchBuildStrategy { /** * The list of filters. */ @NonNull private final List<NameFilter> filters; /** * Our constructor. * @param filters the filters to apply. */ @DataBoundConstructor public NamedBranchBuildStrategyImpl(List<NameFilter> filters) { this.filters = new ArrayList<>(Util.fixNull(filters)); } /** * {@inheritDoc} */ @Deprecated @Override public boolean isAutomaticBuild(@NonNull SCMSource source, @NonNull SCMHead head, @NonNull SCMRevision currRevision, @CheckForNull SCMRevision prevRevision) { return isAutomaticBuild(source, head, currRevision, prevRevision, new LogTaskListener(Logger.getLogger(getClass().getName()), Level.INFO)); } /** * {@inheritDoc} */ @Deprecated @Override public boolean isAutomaticBuild(@NonNull SCMSource source, @NonNull SCMHead head, @NonNull SCMRevision currRevision, @CheckForNull SCMRevision prevRevision, @NonNull TaskListener taskListener) { return isAutomaticBuild(source,head, currRevision, prevRevision, prevRevision, taskListener); } /** * {@inheritDoc} */ @Override public boolean isAutomaticBuild(@NonNull SCMSource source, @NonNull SCMHead head, @NonNull SCMRevision currRevision, @CheckForNull SCMRevision lastBuiltRevision, @CheckForNull SCMRevision lastSeenRevision, @NonNull TaskListener taskListener) { if (head instanceof ChangeRequestSCMHead) { return false; } if (head instanceof TagSCMHead) { return false; } String name = head.getName(); for (NameFilter filter: filters) { if (filter.isMatch(name)) { return true; } } return false; } @NonNull public List<NameFilter> getFilters() { return Collections.unmodifiableList(filters); } /** * {@inheritDoc} */ @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } NamedBranchBuildStrategyImpl that = (NamedBranchBuildStrategyImpl) o; return filters.equals(that.filters); } /** * {@inheritDoc} */ @Override public int hashCode() { return filters.hashCode(); } @Override public String toString() { return "NamedBranchBuildStrategyImpl{" + "filters=" + filters + '}'; } /** * Our descriptor. */ @Symbol("buildNamedBranches") @Extension public static class DescriptorImpl extends BranchBuildStrategyDescriptor { /** * {@inheritDoc} */ @NonNull @Override public String getDisplayName() { return Messages.NamedBranchBuildStrategyImpl_displayName(); } } public static abstract class NameFilter extends AbstractDescribableImpl<NameFilter> { public abstract boolean isMatch(@NonNull String name); @Override public abstract int hashCode(); @Override public abstract boolean equals(Object obj); @Override public abstract String toString(); } public static abstract class NameFilterDescriptor extends Descriptor<NameFilter> { } public static class ExactNameFilter extends NameFilter { @NonNull private final String name; private final boolean caseSensitive; @DataBoundConstructor public ExactNameFilter(@CheckForNull String name, boolean caseSensitive) { this.name = Util.fixNull(name); this.caseSensitive = caseSensitive; } @NonNull public String getName() { return name; } public boolean isCaseSensitive() { return caseSensitive; } @Override public boolean isMatch(@NonNull String name) { return this.caseSensitive ? this.name.equals(name) : this.name.equalsIgnoreCase(name); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ExactNameFilter that = (ExactNameFilter) o; if (caseSensitive != that.caseSensitive) { return false; } return name.equals(that.name); } @Override public int hashCode() { int result = name.hashCode(); result = 31 * result + (caseSensitive ? 1 : 0); return result; } @Override public String toString() { return "ExactNameFilter{" + "name='" + name + '\'' + ", caseSensitive=" + caseSensitive + '}'; } @Symbol("exact") @Extension public static class DescriptorImpl extends NameFilterDescriptor { @Nonnull @Override public String getDisplayName() { return Messages.NamedBranchBuildStrategyImpl_exactDisplayName(); } } } public static class RegexNameFilter extends NameFilter { @NonNull private final String regex; private final boolean caseSensitive; private transient Pattern pattern; @DataBoundConstructor public RegexNameFilter(@CheckForNull String regex, boolean caseSensitive) { this.regex = StringUtils.defaultIfBlank(regex, "^.*$"); this.caseSensitive = caseSensitive; pattern = Pattern.compile(this.regex, caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); } @NonNull public String getRegex() { return regex; } public boolean isCaseSensitive() { return caseSensitive; } @Override public boolean isMatch(@NonNull String name) { if (pattern == null) { pattern = Pattern.compile(regex, caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); } return pattern.matcher(name).matches(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } RegexNameFilter that = (RegexNameFilter) o; if (caseSensitive != that.caseSensitive) { return false; } return regex.equals(that.regex); } @Override public int hashCode() { int result = regex.hashCode(); result = 31 * result + (caseSensitive ? 1 : 0); return result; } @Override public String toString() { return "RegexNameFilter{" + "regex=/" + regex + '/' + ", caseSensitive=" + caseSensitive + '}'; } @Symbol("regex") @Extension public static class DescriptorImpl extends NameFilterDescriptor { @Nonnull @Override public String getDisplayName() { return Messages.NamedBranchBuildStrategyImpl_regexDisplayName(); } /** * Form validation for the regular expression. * * @param value the regular expression. * @return the validation results. */ @Restricted(NoExternalUse.class) // stapler public FormValidation doCheckRegex(@QueryParameter String value) { try { Pattern.compile(value); return FormValidation.ok(); } catch (PatternSyntaxException e) { return FormValidation.error(e.getMessage()); } } } } public static class WildcardsNameFilter extends NameFilter { @NonNull private final String includes; @NonNull private final String excludes; private final boolean caseSensitive; private transient Pattern includePattern; private transient Pattern excludePattern; @DataBoundConstructor public WildcardsNameFilter(@CheckForNull String includes, @CheckForNull String excludes, boolean caseSensitive) { this.includes = StringUtils.defaultIfBlank(includes, "*"); this.excludes = StringUtils.defaultIfBlank(excludes, ""); this.caseSensitive = caseSensitive; includePattern = Pattern.compile(getPattern(this.includes), caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); excludePattern = Pattern.compile(getPattern(this.excludes), caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); } @NonNull public String getIncludes() { return includes; } @NonNull public String getExcludes() { return excludes; } public boolean isCaseSensitive() { return caseSensitive; } @Override public boolean isMatch(@NonNull String name) { if (includePattern == null) { includePattern = Pattern.compile(getPattern(includes), caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); } if (!includePattern.matcher(name).matches()) { return false; } if (StringUtils.isBlank(excludes)) { return true; } if (excludePattern == null) { excludePattern = Pattern.compile(getPattern(excludes), caseSensitive ? 0 : Pattern.CASE_INSENSITIVE); } return !excludePattern.matcher(name).matches(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } WildcardsNameFilter that = (WildcardsNameFilter) o; if (caseSensitive != that.caseSensitive) { return false; } if (!includes.equals(that.includes)) { return false; } return excludes.equals(that.excludes); } @Override public int hashCode() { int result = includes.hashCode(); result = 31 * result + excludes.hashCode(); result = 31 * result + (caseSensitive ? 1 : 0); return result; } @Override public String toString() { return "WildcardsNameFilter{" + "includes='" + includes + '\'' + ", excludes='" + excludes + '\'' + ", caseSensitive=" + caseSensitive + '}'; } /** * Returns the pattern corresponding to the branches containing wildcards. * * @param names the names of branches to create a pattern for * @return pattern corresponding to the branches containing wildcards */ private String getPattern(String names) { StringBuilder quotedBranches = new StringBuilder(); for (String wildcard : names.split(" ")) { StringBuilder quotedBranch = new StringBuilder(); for (String branch : wildcard.split("(?=[*])|(?<=[*])")) { if (branch.equals("*")) { quotedBranch.append(".*"); } else if (!branch.isEmpty()) { quotedBranch.append(Pattern.quote(branch)); } } if (quotedBranches.length() > 0) { quotedBranches.append("|"); } quotedBranches.append(quotedBranch); } return quotedBranches.toString(); } @Symbol("wildcards") @Extension public static class DescriptorImpl extends NameFilterDescriptor { @Nonnull @Override public String getDisplayName() { return Messages.NamedBranchBuildStrategyImpl_wildcardDisplayName(); } } } }