/* * The MIT License * * Copyright 2015-2017 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 org.jenkinsci.plugins.github_branch_source; import com.cloudbees.jenkins.GitHubWebHook; import com.cloudbees.plugins.credentials.CredentialsNameProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.AbortException; import hudson.Extension; import hudson.RestrictedSince; import hudson.Util; import hudson.console.HyperlinkNote; import hudson.model.Action; import hudson.model.Item; import hudson.model.TaskListener; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.inject.Inject; import jenkins.model.Jenkins; import jenkins.plugins.git.traits.GitBrowserSCMSourceTrait; import jenkins.scm.api.SCMNavigator; import jenkins.scm.api.SCMNavigatorDescriptor; import jenkins.scm.api.SCMNavigatorEvent; import jenkins.scm.api.SCMNavigatorOwner; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCategory; import jenkins.scm.api.SCMSourceObserver; import jenkins.scm.api.metadata.ObjectMetadataAction; import jenkins.scm.api.mixin.ChangeRequestCheckoutStrategy; import jenkins.scm.api.trait.SCMHeadAuthority; import jenkins.scm.api.trait.SCMNavigatorRequest; import jenkins.scm.api.trait.SCMNavigatorTrait; import jenkins.scm.api.trait.SCMNavigatorTraitDescriptor; import jenkins.scm.api.trait.SCMSourceTrait; import jenkins.scm.api.trait.SCMTrait; import jenkins.scm.api.trait.SCMTraitDescriptor; import jenkins.scm.impl.UncategorizedSCMSourceCategory; import jenkins.scm.impl.form.NamedArrayList; import jenkins.scm.impl.trait.Discovery; import jenkins.scm.impl.trait.RegexSCMSourceFilterTrait; import jenkins.scm.impl.trait.Selection; import jenkins.scm.impl.trait.WildcardSCMHeadFilterTrait; import net.jcip.annotations.GuardedBy; import org.apache.commons.lang.StringUtils; import org.jenkins.ui.icon.Icon; import org.jenkins.ui.icon.IconSet; import org.jenkins.ui.icon.IconSpec; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.github.config.GitHubServerConfig; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.github.GHMyself; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHUser; import org.kohsuke.github.GitHub; import org.kohsuke.github.HttpException; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.interceptor.RequirePOST; import static org.jenkinsci.plugins.github_branch_source.Connector.isCredentialValid; public class GitHubSCMNavigator extends SCMNavigator { /** * The owner of the repositories to navigate. */ @NonNull private final String repoOwner; /** * The API endpoint for the GitHub server. */ @CheckForNull private String apiUri; /** * The credentials to use when accessing {@link #apiUri} (and also the default credentials to use for checking out). */ @CheckForNull private String credentialsId; /** * The behavioural traits to apply. */ @NonNull private List<SCMTrait<? extends SCMTrait<?>>> traits; /** * Legacy configuration field * * @deprecated use {@link #credentialsId}. */ @Deprecated private transient String scanCredentialsId; /** * Legacy configuration field * * @deprecated use {@link SSHCheckoutTrait}. */ @Deprecated private transient String checkoutCredentialsId; /** * Legacy configuration field * * @deprecated use {@link RegexSCMSourceFilterTrait}. */ @Deprecated private transient String pattern; /** * Legacy configuration field * * @deprecated use {@link WildcardSCMHeadFilterTrait}. */ @Deprecated private String includes; /** * Legacy configuration field * * @deprecated use {@link WildcardSCMHeadFilterTrait}. */ @Deprecated private String excludes; /** * Legacy configuration field * * @deprecated use {@link BranchDiscoveryTrait}. */ @Deprecated private transient Boolean buildOriginBranch; /** * Legacy configuration field * * @deprecated use {@link BranchDiscoveryTrait}. */ @Deprecated private transient Boolean buildOriginBranchWithPR; /** * Legacy configuration field * * @deprecated use {@link OriginPullRequestDiscoveryTrait}. */ @Deprecated private transient Boolean buildOriginPRMerge; /** * Legacy configuration field * * @deprecated use {@link OriginPullRequestDiscoveryTrait}. */ @Deprecated private transient Boolean buildOriginPRHead; /** * Legacy configuration field * * @deprecated use {@link ForkPullRequestDiscoveryTrait}. */ @Deprecated private transient Boolean buildForkPRMerge; /** * Legacy configuration field * * @deprecated use {@link ForkPullRequestDiscoveryTrait}. */ @Deprecated private transient Boolean buildForkPRHead; /** * Constructor. * * @param repoOwner the owner of the repositories to navigate. * @since 2.2.0 */ @DataBoundConstructor public GitHubSCMNavigator(String repoOwner) { this.repoOwner = StringUtils.defaultString(repoOwner); this.traits = new ArrayList<>(); } /** * Legacy constructor. * * @param apiUri the API endpoint for the GitHub server. * @param repoOwner the owner of the repositories to navigate. * @param scanCredentialsId the credentials to use when accessing {@link #apiUri} (and also the default * credentials to use for checking out). * @param checkoutCredentialsId the credentials to use when checking out. * @deprecated use {@link #GitHubSCMNavigator(String)}, {@link #setApiUri(String)}, * {@link #setCredentialsId(String)} and {@link SSHCheckoutTrait} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public GitHubSCMNavigator(String apiUri, String repoOwner, String scanCredentialsId, String checkoutCredentialsId) { this(repoOwner); setCredentialsId(scanCredentialsId); setApiUri(apiUri); // legacy constructor means legacy defaults this.traits = new ArrayList<>(); this.traits.add(new BranchDiscoveryTrait(true, true)); this.traits.add(new ForkPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE), new ForkPullRequestDiscoveryTrait.TrustPermission())); if (!GitHubSCMSource.DescriptorImpl.SAME.equals(checkoutCredentialsId)) { traits.add(new SSHCheckoutTrait(checkoutCredentialsId)); } } /** * Gets the API endpoint for the GitHub server. * * @return the API endpoint for the GitHub server. */ @CheckForNull public String getApiUri() { return apiUri; } /** * Sets the API endpoint for the GitHub server. * * @param apiUri the API endpoint for the GitHub server. * @since 2.2.0 */ @DataBoundSetter public void setApiUri(String apiUri) { apiUri = GitHubConfiguration.normalizeApiUri(Util.fixEmptyAndTrim(apiUri)); this.apiUri = GitHubServerConfig.GITHUB_URL.equals(apiUri) ? null : apiUri; } /** * Gets the {@link StandardCredentials#getId()} of the credentials to use when accessing {@link #apiUri} (and also * the default credentials to use for checking out). * * @return the {@link StandardCredentials#getId()} of the credentials to use when accessing {@link #apiUri} (and * also the default credentials to use for checking out). * @since 2.2.0 */ @CheckForNull public String getCredentialsId() { return credentialsId; } /** * Sets the {@link StandardCredentials#getId()} of the credentials to use when accessing {@link #apiUri} (and also * the default credentials to use for checking out). * * @param credentialsId the {@link StandardCredentials#getId()} of the credentials to use when accessing * {@link #apiUri} (and also the default credentials to use for checking out). * @since 2.2.0 */ @DataBoundSetter public void setCredentialsId(@CheckForNull String credentialsId) { this.credentialsId = Util.fixEmpty(credentialsId); } /** * Gets the name of the owner who's repositories will be navigated. * @return the name of the owner who's repositories will be navigated. */ @NonNull public String getRepoOwner() { return repoOwner; } /** * Gets the behavioural traits that are applied to this navigator and any {@link GitHubSCMSource} instances it * discovers. * * @return the behavioural traits. */ @NonNull public List<SCMTrait<? extends SCMTrait<?>>> getTraits() { return Collections.unmodifiableList(traits); } /** * Sets the behavioural traits that are applied to this navigator and any {@link GitHubSCMSource} instances it * discovers. The new traits will take affect on the next navigation through any of the * {@link #visitSources(SCMSourceObserver)} overloads or {@link #visitSource(String, SCMSourceObserver)}. * * @param traits the new behavioural traits. */ @SuppressWarnings("unchecked") @DataBoundSetter public void setTraits(@CheckForNull SCMTrait[] traits) { // the reduced generics in the method signature are a workaround for JENKINS-26535 this.traits = new ArrayList<>(); if (traits != null) { for (SCMTrait trait : traits) { this.traits.add(trait); } } } /** * Sets the behavioural traits that are applied to this navigator and any {@link GitHubSCMSource} instances it * discovers. The new traits will take affect on the next navigation through any of the * {@link #visitSources(SCMSourceObserver)} overloads or {@link #visitSource(String, SCMSourceObserver)}. * * @param traits the new behavioural traits. */ @Override public void setTraits(@CheckForNull List<SCMTrait<? extends SCMTrait<?>>> traits) { this.traits = traits != null ? new ArrayList<>(traits) : new ArrayList<>(); } /** * Use defaults for old settings. */ @SuppressWarnings("ConstantConditions") @SuppressFBWarnings(value="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification="Only non-null after we set them here!") private Object readResolve() { if (scanCredentialsId != null) { credentialsId = scanCredentialsId; } if (traits == null) { boolean buildOriginBranch = this.buildOriginBranch == null || this.buildOriginBranch; boolean buildOriginBranchWithPR = this.buildOriginBranchWithPR == null || this.buildOriginBranchWithPR; boolean buildOriginPRMerge = this.buildOriginPRMerge != null && this.buildOriginPRMerge; boolean buildOriginPRHead = this.buildOriginPRHead != null && this.buildOriginPRHead; boolean buildForkPRMerge = this.buildForkPRMerge == null || this.buildForkPRMerge; boolean buildForkPRHead = this.buildForkPRHead != null && this.buildForkPRHead; List<SCMTrait<? extends SCMTrait<?>>> traits = new ArrayList<>(); if (buildOriginBranch || buildOriginBranchWithPR) { traits.add(new BranchDiscoveryTrait(buildOriginBranch, buildOriginBranchWithPR)); } if (buildOriginPRMerge || buildOriginPRHead) { EnumSet<ChangeRequestCheckoutStrategy> s = EnumSet.noneOf(ChangeRequestCheckoutStrategy.class); if (buildOriginPRMerge) { s.add(ChangeRequestCheckoutStrategy.MERGE); } if (buildOriginPRHead) { s.add(ChangeRequestCheckoutStrategy.HEAD); } traits.add(new OriginPullRequestDiscoveryTrait(s)); } if (buildForkPRMerge || buildForkPRHead) { EnumSet<ChangeRequestCheckoutStrategy> s = EnumSet.noneOf(ChangeRequestCheckoutStrategy.class); if (buildForkPRMerge) { s.add(ChangeRequestCheckoutStrategy.MERGE); } if (buildForkPRHead) { s.add(ChangeRequestCheckoutStrategy.HEAD); } traits.add(new ForkPullRequestDiscoveryTrait(s, new ForkPullRequestDiscoveryTrait.TrustPermission())); } if (checkoutCredentialsId != null && !DescriptorImpl.SAME.equals(checkoutCredentialsId) && !checkoutCredentialsId.equals(scanCredentialsId)) { traits.add(new SSHCheckoutTrait(checkoutCredentialsId)); } if ((includes != null && !"*".equals(includes)) || (excludes != null && !"".equals(excludes))) { traits.add(new WildcardSCMHeadFilterTrait( StringUtils.defaultIfBlank(includes, "*"), StringUtils.defaultIfBlank(excludes, ""))); } if (pattern != null && !".*".equals(pattern)) { traits.add(new RegexSCMSourceFilterTrait(pattern)); } this.traits = traits; } if (!StringUtils.equals(apiUri, GitHubConfiguration.normalizeApiUri(apiUri))) { setApiUri(apiUri); } return this; } /** * Legacy getter. * * @return {@link #getCredentialsId()}. * @deprecated use {@link #getCredentialsId()}. */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") @CheckForNull public String getScanCredentialsId() { return credentialsId; } /** * Legacy setter. * * @param scanCredentialsId the credentials. * @deprecated use {@link #setCredentialsId(String)} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") @DataBoundSetter public void setScanCredentialsId(@CheckForNull String scanCredentialsId) { this.credentialsId = scanCredentialsId; } /** * Legacy getter. * * @return {@link WildcardSCMHeadFilterTrait#getIncludes()} * @deprecated use {@link WildcardSCMHeadFilterTrait}. */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") @NonNull public String getIncludes() { for (SCMTrait<?> trait : traits) { if (trait instanceof WildcardSCMHeadFilterTrait) { return ((WildcardSCMHeadFilterTrait) trait).getIncludes(); } } return "*"; } /** * Legacy getter. * * @return {@link WildcardSCMHeadFilterTrait#getExcludes()} * @deprecated use {@link WildcardSCMHeadFilterTrait}. */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") @NonNull public String getExcludes() { for (SCMTrait<?> trait : traits) { if (trait instanceof WildcardSCMHeadFilterTrait) { return ((WildcardSCMHeadFilterTrait) trait).getExcludes(); } } return ""; } /** * Legacy setter. * * @param includes see {@link WildcardSCMHeadFilterTrait#WildcardSCMHeadFilterTrait(String, String)} * @deprecated use {@link WildcardSCMHeadFilterTrait}. */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") @DataBoundSetter public void setIncludes(@NonNull String includes) { for (int i = 0; i < traits.size(); i++) { SCMTrait<?> trait = traits.get(i); if (trait instanceof WildcardSCMHeadFilterTrait) { WildcardSCMHeadFilterTrait existing = (WildcardSCMHeadFilterTrait) trait; if ("*".equals(includes) && "".equals(existing.getExcludes())) { traits.remove(i); } else { traits.set(i, new WildcardSCMHeadFilterTrait(includes, existing.getExcludes())); } return; } } if (!"*".equals(includes)) { traits.add(new WildcardSCMHeadFilterTrait(includes, "")); } } /** * Legacy setter. * * @param excludes see {@link WildcardSCMHeadFilterTrait#WildcardSCMHeadFilterTrait(String, String)} * @deprecated use {@link WildcardSCMHeadFilterTrait}. */ @Deprecated @Restricted(NoExternalUse.class) @RestrictedSince("2.2.0") @DataBoundSetter public void setExcludes(@NonNull String excludes) { for (int i = 0; i < traits.size(); i++) { SCMTrait<?> trait = traits.get(i); if (trait instanceof WildcardSCMHeadFilterTrait) { WildcardSCMHeadFilterTrait existing = (WildcardSCMHeadFilterTrait) trait; if ("*".equals(existing.getIncludes()) && "".equals(excludes)) { traits.remove(i); } else { traits.set(i, new WildcardSCMHeadFilterTrait(existing.getIncludes(), excludes)); } return; } } if (!"".equals(excludes)) { traits.add(new WildcardSCMHeadFilterTrait("*", excludes)); } } /** * Legacy getter. * @return {@link BranchDiscoveryTrait#isBuildBranch()}. * @deprecated use {@link BranchDiscoveryTrait} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public boolean getBuildOriginBranch() { for (SCMTrait<?> trait : traits) { if (trait instanceof BranchDiscoveryTrait) { return ((BranchDiscoveryTrait) trait).isBuildBranch(); } } return false; } /** * Legacy setter. * * @param buildOriginBranch see {@link BranchDiscoveryTrait#BranchDiscoveryTrait(boolean, boolean)}. * @deprecated use {@link BranchDiscoveryTrait} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") @DataBoundSetter public void setBuildOriginBranch(boolean buildOriginBranch) { for (int i = 0; i < traits.size(); i++) { SCMTrait<?> trait = traits.get(i); if (trait instanceof BranchDiscoveryTrait) { BranchDiscoveryTrait previous = (BranchDiscoveryTrait) trait; if (buildOriginBranch || previous.isBuildBranchesWithPR()) { traits.set(i, new BranchDiscoveryTrait(buildOriginBranch, previous.isBuildBranchesWithPR())); } else { traits.remove(i); } return; } } if (buildOriginBranch) { traits.add(new BranchDiscoveryTrait(buildOriginBranch, false)); } } /** * Legacy getter. * * @return {@link BranchDiscoveryTrait#isBuildBranchesWithPR()}. * @deprecated use {@link BranchDiscoveryTrait} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public boolean getBuildOriginBranchWithPR() { for (SCMTrait<?> trait : traits) { if (trait instanceof BranchDiscoveryTrait) { return ((BranchDiscoveryTrait) trait).isBuildBranchesWithPR(); } } return false; } /** * Legacy setter. * * @param buildOriginBranchWithPR see {@link BranchDiscoveryTrait#BranchDiscoveryTrait(boolean, boolean)}. * @deprecated use {@link BranchDiscoveryTrait} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") @DataBoundSetter public void setBuildOriginBranchWithPR(boolean buildOriginBranchWithPR) { for (int i = 0; i < traits.size(); i++) { SCMTrait<?> trait = traits.get(i); if (trait instanceof BranchDiscoveryTrait) { BranchDiscoveryTrait previous = (BranchDiscoveryTrait) trait; if (buildOriginBranchWithPR || previous.isBuildBranch()) { traits.set(i, new BranchDiscoveryTrait(previous.isBuildBranch(), buildOriginBranchWithPR)); } else { traits.remove(i); } return; } } if (buildOriginBranchWithPR) { traits.add(new BranchDiscoveryTrait(false, buildOriginBranchWithPR)); } } /** * Legacy getter. * * @return {@link OriginPullRequestDiscoveryTrait#getStrategies()}. * @deprecated use {@link OriginPullRequestDiscoveryTrait#getStrategies()} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public boolean getBuildOriginPRMerge() { for (SCMTrait<?> trait : traits) { if (trait instanceof OriginPullRequestDiscoveryTrait) { return ((OriginPullRequestDiscoveryTrait) trait).getStrategies() .contains(ChangeRequestCheckoutStrategy.MERGE); } } return false; } /** * Legacy setter. * * @param buildOriginPRMerge see {@link OriginPullRequestDiscoveryTrait#OriginPullRequestDiscoveryTrait(Set)}. * @deprecated use {@link OriginPullRequestDiscoveryTrait} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") @DataBoundSetter public void setBuildOriginPRMerge(boolean buildOriginPRMerge) { for (int i = 0; i < traits.size(); i++) { SCMTrait<?> trait = traits.get(i); if (trait instanceof OriginPullRequestDiscoveryTrait) { Set<ChangeRequestCheckoutStrategy> s = ((OriginPullRequestDiscoveryTrait) trait).getStrategies(); if (buildOriginPRMerge) { s.add(ChangeRequestCheckoutStrategy.MERGE); } else { s.remove(ChangeRequestCheckoutStrategy.MERGE); } traits.set(i, new OriginPullRequestDiscoveryTrait(s)); return; } } if (buildOriginPRMerge) { traits.add(new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE))); } } /** * Legacy getter. * * @return {@link OriginPullRequestDiscoveryTrait#getStrategies()}. * @deprecated use {@link OriginPullRequestDiscoveryTrait#getStrategies()} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public boolean getBuildOriginPRHead() { for (SCMTrait<?> trait : traits) { if (trait instanceof OriginPullRequestDiscoveryTrait) { return ((OriginPullRequestDiscoveryTrait) trait).getStrategies() .contains(ChangeRequestCheckoutStrategy.HEAD); } } return false; } /** * Legacy setter. * * @param buildOriginPRHead see {@link OriginPullRequestDiscoveryTrait#OriginPullRequestDiscoveryTrait(Set)}. * @deprecated use {@link OriginPullRequestDiscoveryTrait} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") @DataBoundSetter public void setBuildOriginPRHead(boolean buildOriginPRHead) { for (int i = 0; i < traits.size(); i++) { SCMTrait<?> trait = traits.get(i); if (trait instanceof OriginPullRequestDiscoveryTrait) { Set<ChangeRequestCheckoutStrategy> s = ((OriginPullRequestDiscoveryTrait) trait).getStrategies(); if (buildOriginPRHead) { s.add(ChangeRequestCheckoutStrategy.HEAD); } else { s.remove(ChangeRequestCheckoutStrategy.HEAD); } traits.set(i, new OriginPullRequestDiscoveryTrait(s)); return; } } if (buildOriginPRHead) { traits.add(new OriginPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD))); } } /** * Legacy getter. * * @return {@link ForkPullRequestDiscoveryTrait#getStrategies()}. * @deprecated use {@link ForkPullRequestDiscoveryTrait#getStrategies()} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public boolean getBuildForkPRMerge() { for (SCMTrait<?> trait : traits) { if (trait instanceof ForkPullRequestDiscoveryTrait) { return ((ForkPullRequestDiscoveryTrait) trait).getStrategies() .contains(ChangeRequestCheckoutStrategy.MERGE); } } return false; } /** * Legacy setter. * * @param buildForkPRMerge see {@link ForkPullRequestDiscoveryTrait#ForkPullRequestDiscoveryTrait(Set, SCMHeadAuthority)}. * @deprecated use {@link ForkPullRequestDiscoveryTrait} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") @DataBoundSetter public void setBuildForkPRMerge(boolean buildForkPRMerge) { for (int i = 0; i < traits.size(); i++) { SCMTrait<?> trait = traits.get(i); if (trait instanceof ForkPullRequestDiscoveryTrait) { ForkPullRequestDiscoveryTrait forkTrait = (ForkPullRequestDiscoveryTrait) trait; Set<ChangeRequestCheckoutStrategy> s = forkTrait.getStrategies(); if (buildForkPRMerge) { s.add(ChangeRequestCheckoutStrategy.MERGE); } else { s.remove(ChangeRequestCheckoutStrategy.MERGE); } traits.set(i, new ForkPullRequestDiscoveryTrait(s, forkTrait.getTrust())); return; } } if (buildForkPRMerge) { traits.add(new ForkPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.MERGE), new ForkPullRequestDiscoveryTrait.TrustPermission())); } } /** * Legacy getter. * * @return {@link ForkPullRequestDiscoveryTrait#getStrategies()}. * @deprecated use {@link ForkPullRequestDiscoveryTrait#getStrategies()} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public boolean getBuildForkPRHead() { for (SCMTrait<?> trait : traits) { if (trait instanceof ForkPullRequestDiscoveryTrait) { return ((ForkPullRequestDiscoveryTrait) trait).getStrategies() .contains(ChangeRequestCheckoutStrategy.HEAD); } } return false; } /** * Legacy setter. * * @param buildForkPRHead see * {@link ForkPullRequestDiscoveryTrait#ForkPullRequestDiscoveryTrait(Set, SCMHeadAuthority)}. * @deprecated use {@link ForkPullRequestDiscoveryTrait} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") @DataBoundSetter public void setBuildForkPRHead(boolean buildForkPRHead) { for (int i = 0; i < traits.size(); i++) { SCMTrait<?> trait = traits.get(i); if (trait instanceof ForkPullRequestDiscoveryTrait) { ForkPullRequestDiscoveryTrait forkTrait = (ForkPullRequestDiscoveryTrait) trait; Set<ChangeRequestCheckoutStrategy> s = forkTrait.getStrategies(); if (buildForkPRHead) { s.add(ChangeRequestCheckoutStrategy.HEAD); } else { s.remove(ChangeRequestCheckoutStrategy.HEAD); } traits.set(i, new ForkPullRequestDiscoveryTrait(s, forkTrait.getTrust())); return; } } if (buildForkPRHead) { traits.add(new ForkPullRequestDiscoveryTrait(EnumSet.of(ChangeRequestCheckoutStrategy.HEAD), new ForkPullRequestDiscoveryTrait.TrustPermission())); } } /** * Legacy getter. * * @return {@link SSHCheckoutTrait#getCredentialsId()} with some mangling to preserve legacy behaviour. * @deprecated use {@link SSHCheckoutTrait} */ @CheckForNull @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public String getCheckoutCredentialsId() { for (SCMTrait<?> trait : traits) { if (trait instanceof SSHCheckoutTrait) { return StringUtils.defaultString( ((SSHCheckoutTrait) trait).getCredentialsId(), GitHubSCMSource.DescriptorImpl.ANONYMOUS ); } } return DescriptorImpl.SAME; } /** * Legacy getter. * * @return {@link RegexSCMSourceFilterTrait#getRegex()}. * @deprecated use {@link RegexSCMSourceFilterTrait} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public String getPattern() { for (SCMTrait<?> trait : traits) { if (trait instanceof RegexSCMSourceFilterTrait) { return ((RegexSCMSourceFilterTrait) trait).getRegex(); } } return ".*"; } /** * Legacy setter. * * @param pattern see {@link RegexSCMSourceFilterTrait#RegexSCMSourceFilterTrait(String)}. * @deprecated use {@link RegexSCMSourceFilterTrait} */ @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") @DataBoundSetter public void setPattern(String pattern) { for (int i = 0; i < traits.size(); i++) { SCMTrait<?> trait = traits.get(i); if (trait instanceof RegexSCMSourceFilterTrait) { if (".*".equals(pattern)) { traits.remove(i); } else { traits.set(i, new RegexSCMSourceFilterTrait(pattern)); } return; } } if (!".*".equals(pattern)) { traits.add(new RegexSCMSourceFilterTrait(pattern)); } } /** * {@inheritDoc} */ @NonNull @Override protected String id() { return StringUtils.defaultIfBlank(apiUri, GitHubSCMSource.GITHUB_URL) + "::" + repoOwner; } /** * {@inheritDoc} */ @Override public void visitSources(SCMSourceObserver observer) throws IOException, InterruptedException { Set<String> includes = observer.getIncludes(); if (includes != null && includes.size() == 1) { // optimize for the single source case visitSource(includes.iterator().next(), observer); return; } TaskListener listener = observer.getListener(); // Input data validation if (repoOwner.isEmpty()) { throw new AbortException("Must specify user or organization"); } StandardCredentials credentials = Connector.lookupScanCredentials((Item)observer.getContext(), apiUri, credentialsId); // Github client and validation GitHub github = Connector.connect(apiUri, credentials); try { Connector.checkConnectionValidity(apiUri, listener, credentials, github); Connector.checkApiRateLimit(listener, github); // Input data validation if (credentials != null && !isCredentialValid(github)) { String message = String.format("Invalid scan credentials %s to connect to %s, skipping", CredentialsNameProvider.name(credentials), apiUri == null ? GitHubSCMSource.GITHUB_URL : apiUri); throw new AbortException(message); } GitHubSCMNavigatorContext gitHubSCMNavigatorContext = new GitHubSCMNavigatorContext().withTraits(traits); try (GitHubSCMNavigatorRequest request = gitHubSCMNavigatorContext.newRequest(this, observer)) { SourceFactory sourceFactory = new SourceFactory(request); WitnessImpl witness = new WitnessImpl(listener); boolean githubAppAuthentication = credentials instanceof GitHubAppCredentials; if (github.isAnonymous()) { listener.getLogger().format("Connecting to %s with no credentials, anonymous access%n", apiUri == null ? GitHubSCMSource.GITHUB_URL : apiUri); } else if (!githubAppAuthentication) { GHMyself myself; try { // Requires an authenticated access myself = github.getMyself(); } catch (RateLimitExceededException rle) { throw new AbortException(rle.getMessage()); } if (myself != null && repoOwner.equalsIgnoreCase(myself.getLogin())) { listener.getLogger() .println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "Looking up repositories of myself %s", repoOwner ))); for (GHRepository repo : myself.listRepositories(100)) { Connector.checkApiRateLimit(listener, github); if (!repo.getOwnerName().equals(repoOwner)) { continue; // ignore repos in other orgs when using GHMyself } if (repo.isArchived() && gitHubSCMNavigatorContext.isExcludeArchivedRepositories()) { witness.record(repo.getName(), false); listener.getLogger() .println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "Skipping repository %s because it is archived", repo.getName()))); } else if (request.process(repo.getName(), sourceFactory, null, witness)) { listener.getLogger() .println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "%d repositories were processed (query completed)", witness.getCount() ))); } } listener.getLogger().println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "%d repositories were processed", witness.getCount() ))); return; } } GHOrganization org = getGhOrganization(github); if (org != null && repoOwner.equalsIgnoreCase(org.getLogin())) { listener.getLogger().println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "Looking up repositories of organization %s", repoOwner))); final Iterable<GHRepository> repositories; if (StringUtils.isNotBlank(gitHubSCMNavigatorContext.getTeamSlug())) { listener.getLogger().println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "Looking up repositories for team %s", gitHubSCMNavigatorContext.getTeamSlug()))); repositories = org.getTeamBySlug(gitHubSCMNavigatorContext.getTeamSlug()).listRepositories().withPageSize(100); } else { repositories = org.listRepositories(100); } for (GHRepository repo : repositories) { Connector.checkApiRateLimit(listener, github); if (repo.isArchived() && gitHubSCMNavigatorContext.isExcludeArchivedRepositories()) { witness.record(repo.getName(), false); listener.getLogger() .println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "Skipping repository %s because it is archived", repo.getName()))); } else if (request.process(repo.getName(), sourceFactory, null, witness)) { listener.getLogger().println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "%d repositories were processed (query completed)", witness.getCount() ))); } } listener.getLogger().println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "%d repositories were processed", witness.getCount()))); return; } GHUser user = null; try { user = github.getUser(repoOwner); } catch (RateLimitExceededException rle) { throw new AbortException(rle.getMessage()); } catch (FileNotFoundException fnf) { // the user may not exist... ok to ignore } if (user != null && repoOwner.equalsIgnoreCase(user.getLogin())) { listener.getLogger().format("Looking up repositories of user %s%n%n", repoOwner); Connector.checkApiRateLimit(listener, github); for (GHRepository repo : user.listRepositories(100)) { Connector.checkApiRateLimit(listener, github); if (repo.isArchived() && gitHubSCMNavigatorContext.isExcludeArchivedRepositories()) { witness.record(repo.getName(), false); listener.getLogger() .println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "Skipping repository %s because it is archived", repo.getName()))); } else if (request.process(repo.getName(), sourceFactory, null, witness)) { listener.getLogger() .println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "%d repositories were processed (query completed)", witness.getCount() ))); } } listener.getLogger().println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "%d repositories were processed", witness.getCount() ))); return; } throw new AbortException( repoOwner + " does not correspond to a known GitHub User Account or Organization"); } } finally { Connector.release(github); } } private GHOrganization getGhOrganization(final GitHub github) throws IOException { try { return github.getOrganization(repoOwner); } catch (RateLimitExceededException rle) { throw new AbortException(rle.getMessage()); } catch (FileNotFoundException fnf) { // may be an user... ok to ignore } return null; } /** * {@inheritDoc} */ @Override public void visitSource(String sourceName, SCMSourceObserver observer) throws IOException, InterruptedException { TaskListener listener = observer.getListener(); // Input data validation if (repoOwner.isEmpty()) { throw new AbortException("Must specify user or organization"); } StandardCredentials credentials = Connector.lookupScanCredentials((Item)observer.getContext(), apiUri, credentialsId); // Github client and validation GitHub github = Connector.connect(apiUri, credentials); try { try { Connector.checkApiUrlValidity(github, credentials); } catch (HttpException e) { String message = String.format("It seems %s is unreachable", apiUri == null ? GitHubSCMSource.GITHUB_URL : apiUri); throw new AbortException(message); } // Input data validation if (credentials != null && !isCredentialValid(github)) { String message = String.format("Invalid scan credentials %s to connect to %s, skipping", CredentialsNameProvider.name(credentials), apiUri == null ? GitHubSCMSource.GITHUB_URL : apiUri); throw new AbortException(message); } GitHubSCMNavigatorContext gitHubSCMNavigatorContext = new GitHubSCMNavigatorContext().withTraits(traits); try (GitHubSCMNavigatorRequest request = gitHubSCMNavigatorContext.newRequest(this, observer)) { SourceFactory sourceFactory = new SourceFactory(request); WitnessImpl witness = new WitnessImpl(listener); boolean githubAppAuthentication = credentials instanceof GitHubAppCredentials; if (github.isAnonymous()) { listener.getLogger().format("Connecting to %s with no credentials, anonymous access%n", apiUri == null ? GitHubSCMSource.GITHUB_URL : apiUri); } else if (!githubAppAuthentication) { listener.getLogger() .format("Connecting to %s using %s%n", apiUri == null ? GitHubSCMSource.GITHUB_URL : apiUri, CredentialsNameProvider.name(credentials)); GHMyself myself; try { // Requires an authenticated access myself = github.getMyself(); } catch (RateLimitExceededException rle) { throw new AbortException(rle.getMessage()); } if (myself != null && repoOwner.equalsIgnoreCase(myself.getLogin())) { listener.getLogger().format("Looking up %s repository of myself %s%n%n", sourceName, repoOwner); GHRepository repo = myself.getRepository(sourceName); if (repo != null && repo.getOwnerName().equals(repoOwner)) { if (repo.isArchived() && gitHubSCMNavigatorContext.isExcludeArchivedRepositories()) { witness.record(repo.getName(), false); listener.getLogger() .println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "Skipping repository %s because it is archived", repo.getName()))); } else if (request.process(repo.getName(), sourceFactory, null, witness)) { listener.getLogger() .println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "%d repositories were processed (query completed)", witness.getCount() ))); } } listener.getLogger().println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "%d repositories were processed", witness.getCount() ))); return; } } GHOrganization org = getGhOrganization(github); if (org != null && repoOwner.equalsIgnoreCase(org.getLogin())) { listener.getLogger() .format("Looking up %s repository of organization %s%n%n", sourceName, repoOwner); GHRepository repo = org.getRepository(sourceName); if (repo != null) { if (repo.isArchived() && gitHubSCMNavigatorContext.isExcludeArchivedRepositories()) { witness.record(repo.getName(), false); listener.getLogger() .println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "Skipping repository %s because it is archived", repo.getName()))); } else if (request.process(repo.getName(), sourceFactory, null, witness)) { listener.getLogger() .println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "%d repositories were processed (query completed)", witness.getCount() ))); } } listener.getLogger().println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "%d repositories were processed", witness.getCount() ))); return; } GHUser user = null; try { user = github.getUser(repoOwner); } catch (RateLimitExceededException rle) { throw new AbortException(rle.getMessage()); } catch (FileNotFoundException fnf) { // the user may not exist... ok to ignore } if (user != null && repoOwner.equalsIgnoreCase(user.getLogin())) { listener.getLogger().format("Looking up %s repository of user %s%n%n", sourceName, repoOwner); GHRepository repo = user.getRepository(sourceName); if (repo != null) { if (repo.isArchived() && gitHubSCMNavigatorContext.isExcludeArchivedRepositories()) { witness.record(repo.getName(), false); listener.getLogger() .println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "Skipping repository %s because it is archived", repo.getName()))); } else if (request.process(repo.getName(), sourceFactory, null, witness)) { listener.getLogger() .println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "%d repositories were processed (query completed)", witness.getCount() ))); } } listener.getLogger().println(GitHubConsoleNote.create(System.currentTimeMillis(), String.format( "%d repositories were processed", witness.getCount() ))); return; } throw new AbortException( repoOwner + " does not correspond to a known GitHub User Account or Organization"); } } finally { Connector.release(github); } } /** * {@inheritDoc} */ @NonNull @Override public List<Action> retrieveActions(@NonNull SCMNavigatorOwner owner, @CheckForNull SCMNavigatorEvent event, @NonNull TaskListener listener) throws IOException, InterruptedException { // TODO when we have support for trusted events, use the details from event if event was from trusted source listener.getLogger().printf("Looking up details of %s...%n", getRepoOwner()); List<Action> result = new ArrayList<>(); StandardCredentials credentials = Connector.lookupScanCredentials((Item)owner, getApiUri(), credentialsId); GitHub hub = Connector.connect(getApiUri(), credentials); try { Connector.checkApiRateLimit(listener, hub); GHUser u = hub.getUser(getRepoOwner()); String objectUrl = u.getHtmlUrl() == null ? null : u.getHtmlUrl().toExternalForm(); result.add(new ObjectMetadataAction( Util.fixEmpty(u.getName()), null, objectUrl) ); result.add(new GitHubOrgMetadataAction(u)); result.add(new GitHubLink("icon-github-logo", u.getHtmlUrl())); if (objectUrl == null) { listener.getLogger().println("Organization URL: unspecified"); } else { listener.getLogger().printf("Organization URL: %s%n", HyperlinkNote.encodeTo(objectUrl, StringUtils.defaultIfBlank(u.getName(), objectUrl))); } return result; } finally { Connector.release(hub); } } /** * {@inheritDoc} */ @Override public void afterSave(@NonNull SCMNavigatorOwner owner) { GitHubWebHook.get().registerHookFor(owner); try { // FIXME MINOR HACK ALERT StandardCredentials credentials = Connector.lookupScanCredentials((Item)owner, getApiUri(), credentialsId); GitHub hub = Connector.connect(getApiUri(), credentials); try { GitHubOrgWebHook.register(hub, repoOwner); } finally { Connector.release(hub); } } catch (IOException e) { DescriptorImpl.LOGGER.log(Level.WARNING, e.getMessage(), e); } } @Symbol("github") @Extension public static class DescriptorImpl extends SCMNavigatorDescriptor implements IconSpec { private static final Logger LOGGER = Logger.getLogger(DescriptorImpl.class.getName()); @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public static final String defaultIncludes = "*"; @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public static final String defaultExcludes = ""; public static final String SAME = GitHubSCMSource.DescriptorImpl.SAME; @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public static final boolean defaultBuildOriginBranch = true; @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public static final boolean defaultBuildOriginBranchWithPR = true; @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public static final boolean defaultBuildOriginPRMerge = false; @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public static final boolean defaultBuildOriginPRHead = false; @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public static final boolean defaultBuildForkPRMerge = false; @Deprecated @Restricted(DoNotUse.class) @RestrictedSince("2.2.0") public static final boolean defaultBuildForkPRHead = false; @Inject private GitHubSCMSource.DescriptorImpl delegate; /** * {@inheritDoc} */ @Override public String getPronoun() { return Messages.GitHubSCMNavigator_Pronoun(); } /** * {@inheritDoc} */ @Override public String getDisplayName() { return Messages.GitHubSCMNavigator_DisplayName(); } /** * {@inheritDoc} */ @Override public String getDescription() { return Messages.GitHubSCMNavigator_Description(); } /** * {@inheritDoc} */ @Override public String getIconFilePathPattern() { return "plugin/github-branch-source/images/:size/github-scmnavigator.png"; } /** * {@inheritDoc} */ @Override public String getIconClassName() { return "icon-github-scm-navigator"; } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public SCMNavigator newInstance(String name) { GitHubSCMNavigator navigator = new GitHubSCMNavigator(name); navigator.setTraits(getTraitsDefaults()); return navigator; } /** * {@inheritDoc} */ @NonNull @Override protected SCMSourceCategory[] createCategories() { return new SCMSourceCategory[]{ new UncategorizedSCMSourceCategory(Messages._GitHubSCMNavigator_UncategorizedCategory()) // TODO add support for forks }; } /** * Validates the selected credentials. * * @param context the context. * @param apiUri the end-point. * @param credentialsId the credentials. * @return validation results. * @since 2.2.0 */ @RequirePOST @Restricted(NoExternalUse.class) // stapler public FormValidation doCheckCredentialsId(@CheckForNull @AncestorInPath Item context, @QueryParameter String apiUri, @QueryParameter String credentialsId) { return Connector.checkScanCredentials(context, apiUri, credentialsId); } /** * Populates the drop-down list of credentials. * * @param context the context. * @param apiUri the end-point. * @param credentialsId the existing selection; * @return the drop-down list. * @since 2.2.0 */ @Restricted(NoExternalUse.class) // stapler public ListBoxModel doFillCredentialsIdItems(@CheckForNull @AncestorInPath Item context, @QueryParameter String apiUri, @QueryParameter String credentialsId) { if (context == null ? !Jenkins.get().hasPermission(Jenkins.ADMINISTER) : !context.hasPermission(Item.EXTENDED_READ)) { return new StandardListBoxModel().includeCurrentValue(credentialsId); } return Connector.listScanCredentials(context, apiUri); } /** * Returns the available GitHub endpoint items. * * @return the available GitHub endpoint items. */ @Restricted(NoExternalUse.class) // stapler @SuppressWarnings("unused") // stapler public ListBoxModel doFillApiUriItems() { return getPossibleApiUriItems(); } static ListBoxModel getPossibleApiUriItems() { ListBoxModel result = new ListBoxModel(); result.add("GitHub", ""); for (Endpoint e : GitHubConfiguration.get().getEndpoints()) { result.add(e.getName() == null ? e.getApiUri() : e.getName() + " (" + e.getApiUri() + ")", e.getApiUri()); } return result; } /** * Returns {@code true} if there is more than one GitHub endpoint configured, and consequently the UI should * provide the ability to select the endpoint. * * @return {@code true} if there is more than one GitHub endpoint configured. */ @SuppressWarnings("unused") // jelly public boolean isApiUriSelectable() { return !GitHubConfiguration.get().getEndpoints().isEmpty(); } /** * Returns the {@link SCMTraitDescriptor} instances grouped into categories. * * @return the categorized list of {@link SCMTraitDescriptor} instances. * @since 2.2.0 */ @SuppressWarnings("unused") // jelly public List<NamedArrayList<? extends SCMTraitDescriptor<?>>> getTraitsDescriptorLists() { GitHubSCMSource.DescriptorImpl sourceDescriptor = Jenkins.get().getDescriptorByType(GitHubSCMSource.DescriptorImpl.class); List<SCMTraitDescriptor<?>> all = new ArrayList<>(); all.addAll(SCMNavigatorTrait._for(this, GitHubSCMNavigatorContext.class, GitHubSCMSourceBuilder.class)); all.addAll(SCMSourceTrait._for(sourceDescriptor, GitHubSCMSourceContext.class, null)); all.addAll(SCMSourceTrait._for(sourceDescriptor, null, GitHubSCMBuilder.class)); Set<SCMTraitDescriptor<?>> dedup = new HashSet<>(); for (Iterator<SCMTraitDescriptor<?>> iterator = all.iterator(); iterator.hasNext(); ) { SCMTraitDescriptor<?> d = iterator.next(); if (dedup.contains(d) || d instanceof GitBrowserSCMSourceTrait.DescriptorImpl) { // remove any we have seen already and ban the browser configuration as it will always be github iterator.remove(); } else { dedup.add(d); } } List<NamedArrayList<? extends SCMTraitDescriptor<?>>> result = new ArrayList<>(); NamedArrayList.select(all, "Repositories", new NamedArrayList.Predicate<SCMTraitDescriptor<?>>() { @Override public boolean test(SCMTraitDescriptor<?> scmTraitDescriptor) { return scmTraitDescriptor instanceof SCMNavigatorTraitDescriptor; } }, true, result); NamedArrayList.select(all, Messages.GitHubSCMNavigator_withinRepository(), NamedArrayList.anyOf(NamedArrayList.withAnnotation(Discovery.class),NamedArrayList.withAnnotation(Selection.class)), true, result); NamedArrayList.select(all, Messages.GitHubSCMNavigator_general(), null, true, result); return result; } @SuppressWarnings("unused") // jelly @NonNull public List<SCMTrait<? extends SCMTrait<?>>> getTraitsDefaults() { return new ArrayList<>(delegate.getTraitsDefaults()); } static { IconSet.icons.addIcon( new Icon("icon-github-scm-navigator icon-sm", "plugin/github-branch-source/images/16x16/github-scmnavigator.png", Icon.ICON_SMALL_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-scm-navigator icon-md", "plugin/github-branch-source/images/24x24/github-scmnavigator.png", Icon.ICON_MEDIUM_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-scm-navigator icon-lg", "plugin/github-branch-source/images/32x32/github-scmnavigator.png", Icon.ICON_LARGE_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-scm-navigator icon-xlg", "plugin/github-branch-source/images/48x48/github-scmnavigator.png", Icon.ICON_XLARGE_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-logo icon-sm", "plugin/github-branch-source/images/16x16/github-logo.png", Icon.ICON_SMALL_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-logo icon-md", "plugin/github-branch-source/images/24x24/github-logo.png", Icon.ICON_MEDIUM_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-logo icon-lg", "plugin/github-branch-source/images/32x32/github-logo.png", Icon.ICON_LARGE_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-logo icon-xlg", "plugin/github-branch-source/images/48x48/github-logo.png", Icon.ICON_XLARGE_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-repo icon-sm", "plugin/github-branch-source/images/16x16/github-repo.png", Icon.ICON_SMALL_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-repo icon-md", "plugin/github-branch-source/images/24x24/github-repo.png", Icon.ICON_MEDIUM_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-repo icon-lg", "plugin/github-branch-source/images/32x32/github-repo.png", Icon.ICON_LARGE_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-repo icon-xlg", "plugin/github-branch-source/images/48x48/github-repo.png", Icon.ICON_XLARGE_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-branch icon-sm", "plugin/github-branch-source/images/16x16/github-branch.png", Icon.ICON_SMALL_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-branch icon-md", "plugin/github-branch-source/images/24x24/github-branch.png", Icon.ICON_MEDIUM_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-branch icon-lg", "plugin/github-branch-source/images/32x32/github-branch.png", Icon.ICON_LARGE_STYLE)); IconSet.icons.addIcon( new Icon("icon-github-branch icon-xlg", "plugin/github-branch-source/images/48x48/github-branch.png", Icon.ICON_XLARGE_STYLE)); } } /** * A {@link SCMNavigatorRequest.Witness} that counts how many sources have been observed. */ private static class WitnessImpl implements SCMNavigatorRequest.Witness { /** * The count of repositories matches. */ @GuardedBy("this") private int count; /** * The listener to log to. */ @NonNull private final TaskListener listener; /** * Constructor. * * @param listener the listener to log to. */ public WitnessImpl(@NonNull TaskListener listener) { this.listener = listener; } /** * {@inheritDoc} */ @Override public void record(@NonNull String name, boolean isMatch) { if (isMatch) { listener.getLogger().format("Proposing %s%n", name); synchronized (this) { count++; } } else { listener.getLogger().format("Ignoring %s%n", name); } } /** * Returns the count of repositories matches. * * @return the count of repositories matches. */ public synchronized int getCount() { return count; } } /** * Our {@link SCMNavigatorRequest.SourceLambda}. */ private class SourceFactory implements SCMNavigatorRequest.SourceLambda { /** * The request. */ private final GitHubSCMNavigatorRequest request; /** * Constructor. * * @param request the request to decorate {@link SCMSource} instances with. */ public SourceFactory(GitHubSCMNavigatorRequest request) { this.request = request; } /** * {@inheritDoc} */ @NonNull @Override public SCMSource create(@NonNull String name) { return new GitHubSCMSourceBuilder(getId() + "::" + name, apiUri, credentialsId, repoOwner, name) .withRequest(request) .build(); } } }