public class GitHubPushTrigger extends Trigger<Job<?, ?>> implements GitHubTrigger {

    @DataBoundConstructor
    public GitHubPushTrigger() {
    }

    /**
     * Called when a POST is made.
     */
    @Deprecated
    public void onPost() {
        onPost(GitHubTriggerEvent.create()
                .build()
        );
    }

    /**
     * Called when a POST is made.
     */
    public void onPost(String triggeredByUser) {
        onPost(GitHubTriggerEvent.create()
                .withOrigin(SCMEvent.originOf(Stapler.getCurrentRequest()))
                .withTriggeredByUser(triggeredByUser)
                .build()
        );
    }

    /**
     * Called when a POST is made.
     */
    public void onPost(final GitHubTriggerEvent event) {
        if (Objects.isNull(job)) {
            return; // nothing to do
        }

        Job<?, ?> currentJob = notNull(job, "Job can't be null");

        final String pushBy = event.getTriggeredByUser();
        DescriptorImpl d = getDescriptor();
        d.checkThreadPoolSizeAndUpdateIfNecessary();
        d.queue.execute(new Runnable() {
            private boolean runPolling() {
                try {
                    StreamTaskListener listener = new StreamTaskListener(getLogFileForJob(currentJob));

                    try {
                        PrintStream logger = listener.getLogger();

                        long start = System.currentTimeMillis();
                        logger.println("Started on " + DateFormat.getDateTimeInstance().format(new Date()));
                        if (event.getOrigin() != null) {
                            logger.format("Started by event from %s on %tc%n", event.getOrigin(), event.getTimestamp());
                        }
                        SCMTriggerItem item = SCMTriggerItems.asSCMTriggerItem(currentJob);
                        if (null == item) {
                            throw new IllegalStateException("Job is not an SCMTriggerItem: " + currentJob);
                        }
                        boolean result = item.poll(listener).hasChanges();
                        logger.println("Done. Took " + Util.getTimeSpanString(System.currentTimeMillis() - start));
                        if (result) {
                            logger.println("Changes found");
                        } else {
                            logger.println("No changes");
                        }
                        return result;
                    } catch (Error e) {
                        e.printStackTrace(listener.error("Failed to record SCM polling"));
                        LOGGER.error("Failed to record SCM polling", e);
                        throw e;
                    } catch (RuntimeException e) {
                        e.printStackTrace(listener.error("Failed to record SCM polling"));
                        LOGGER.error("Failed to record SCM polling", e);
                        throw e;
                    } finally {
                        listener.close();
                    }
                } catch (IOException e) {
                    LOGGER.error("Failed to record SCM polling", e);
                }
                return false;
            }

            public void run() {
                if (runPolling()) {
                    GitHubPushCause cause;
                    try {
                        cause = new GitHubPushCause(getLogFileForJob(currentJob), pushBy);
                    } catch (IOException e) {
                        LOGGER.warn("Failed to parse the polling log", e);
                        cause = new GitHubPushCause(pushBy);
                    }

                    if (asParameterizedJobMixIn(currentJob).scheduleBuild(cause)) {
                        LOGGER.info("SCM changes detected in " + currentJob.getFullName()
                                + ". Triggering #" + currentJob.getNextBuildNumber());
                    } else {
                        LOGGER.info("SCM changes detected in " + currentJob.getFullName()
                                + ". Job is already in the queue");
                    }
                }
            }
        });
    }

    /**
     * Returns the file that records the last/current polling activity.
     */
    public File getLogFile() {
        try {
            return getLogFileForJob(notNull(job, "Job can't be null!"));
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Returns the file that records the last/current polling activity.
     */
    private File getLogFileForJob(@NonNull Job job) throws IOException {
        return new File(job.getRootDir(), "github-polling.log");
    }

    /**
     * @deprecated Use {@link GitHubRepositoryNameContributor#parseAssociatedNames(AbstractProject)}
     */
    @Deprecated
    public Set<GitHubRepositoryName> getGitHubRepositories() {
        return Collections.emptySet();
    }

    @Override
    public void start(Job<?, ?> project, boolean newInstance) {
        super.start(project, newInstance);
        if (newInstance && GitHubPlugin.configuration().isManageHooks()) {
            registerHooks();
        }
    }

    /**
     * Tries to register hook for current associated job.
     * Do this lazily to avoid blocking the UI thread.
     * Useful for using from groovy scripts.
     *
     * @since 1.11.2
     */
    public void registerHooks() {
        GitHubWebHook.get().registerHookFor(job);
    }

    @Override
    public void stop() {
        if (job == null) {
            return;
        }

        if (GitHubPlugin.configuration().isManageHooks()) {
            Cleaner cleaner = Cleaner.get();
            if (cleaner != null) {
                cleaner.onStop(job);
            }
        }
    }

    @Override
    public Collection<? extends Action> getProjectActions() {
        if (job == null) {
            return Collections.emptyList();
        }

        return Collections.singleton(new GitHubWebHookPollingAction());
    }

    @Override
    public DescriptorImpl getDescriptor() {
        return (DescriptorImpl) super.getDescriptor();
    }

    /**
     * Action object for {@link Project}. Used to display the polling log.
     */
    public final class GitHubWebHookPollingAction implements Action {
        public Job<?, ?> getOwner() {
            return job;
        }

        public String getIconFileName() {
            return "clipboard.png";
        }

        public String getDisplayName() {
            return "GitHub Hook Log";
        }

        public String getUrlName() {
            return "GitHubPollLog";
        }

        public String getLog() throws IOException {
            return Util.loadFile(getLogFileForJob(job));
        }

        /**
         * Writes the annotated log to the given output.
         *
         * @since 1.350
         */
        public void writeLogTo(XMLOutput out) throws IOException {
            new AnnotatedLargeText<GitHubWebHookPollingAction>(getLogFileForJob(job), Charsets.UTF_8, true, this)
                    .writeHtmlTo(0, out.asWriter());
        }
    }

    @Extension
    @Symbol("githubPush")
    public static class DescriptorImpl extends TriggerDescriptor {
        private final transient SequentialExecutionQueue queue =
                new SequentialExecutionQueue(Executors.newSingleThreadExecutor(threadFactory()));

        private transient String hookUrl;

        private transient List<Credential> credentials;

        @Inject
        private transient GitHubHookRegisterProblemMonitor monitor;

        @Inject
        private transient SCMTrigger.DescriptorImpl scmTrigger;

        private transient int maximumThreads = Integer.MIN_VALUE;

        public DescriptorImpl() {
            checkThreadPoolSizeAndUpdateIfNecessary();
        }

        /**
         * Update the {@link java.util.concurrent.ExecutorService} instance.
         */
        /*package*/
        synchronized void checkThreadPoolSizeAndUpdateIfNecessary() {
            if (scmTrigger != null) {
                int count = scmTrigger.getPollingThreadCount();
                if (maximumThreads != count) {
                    maximumThreads = count;
                    queue.setExecutors(
                            (count == 0
                                    ? Executors.newCachedThreadPool(threadFactory())
                                    : Executors.newFixedThreadPool(maximumThreads, threadFactory())));
                }
            }
        }

        @Override
        public boolean isApplicable(Item item) {
            return item instanceof Job && SCMTriggerItems.asSCMTriggerItem(item) != null
                    && item instanceof ParameterizedJobMixIn.ParameterizedJob;
        }

        @Override
        public String getDisplayName() {
            return "GitHub hook trigger for GITScm polling";
        }

        /**
         * True if Jenkins should auto-manage hooks.
         *
         * @deprecated Use {@link GitHubPluginConfig#isManageHooks()} instead
         */
        @Deprecated
        public boolean isManageHook() {
            return GitHubPlugin.configuration().isManageHooks();
        }

        /**
         * Returns the URL that GitHub should post.
         *
         * @deprecated use {@link GitHubPluginConfig#getHookUrl()} instead
         */
        @Deprecated
        public URL getHookUrl() throws GHPluginConfigException {
            return GitHubPlugin.configuration().getHookUrl();
        }

        /**
         * @return null after migration
         * @deprecated use {@link GitHubPluginConfig#getConfigs()} instead.
         */
        @Deprecated
        public List<Credential> getCredentials() {
            return credentials;
        }

        /**
         * Used only for migration
         *
         * @return null after migration
         * @deprecated use {@link GitHubPluginConfig#getHookUrl()}
         */
        @Deprecated
        public URL getDeprecatedHookUrl() {
            if (isEmpty(hookUrl)) {
                return null;
            }
            try {
                return new URL(hookUrl);
            } catch (MalformedURLException e) {
                LOGGER.warn("Malformed hook url skipped while migration ({})", e.getMessage());
                return null;
            }
        }

        /**
         * Used to cleanup after migration
         */
        public void clearDeprecatedHookUrl() {
            this.hookUrl = null;
        }

        /**
         * Used to cleanup after migration
         */
        public void clearCredentials() {
            this.credentials = null;
        }

        /**
         * @deprecated use {@link GitHubPluginConfig#isOverrideHookUrl()}
         */
        @Deprecated
        public boolean hasOverrideURL() {
            return GitHubPlugin.configuration().isOverrideHookUrl();
        }

        /**
         * Uses global xstream to enable migration alias used in
         * {@link Migrator#enableCompatibilityAliases()}
         */
        @Override
        protected XmlFile getConfigFile() {
            return new XmlFile(Jenkins.XSTREAM2, super.getConfigFile().getFile());
        }

        public static DescriptorImpl get() {
            return Trigger.all().get(DescriptorImpl.class);
        }

        public static boolean allowsHookUrlOverride() {
            return ALLOW_HOOKURL_OVERRIDE;
        }

        private static ThreadFactory threadFactory() {
            return new NamingThreadFactory(Executors.defaultThreadFactory(), "GitHubPushTrigger");
        }

        /**
         * Checks that repo defined in this item is not in administrative monitor as failed to be registered.
         * If that so, shows warning with some instructions
         *
         * @param item - to check against. Should be not null and have at least one repo defined
         *
         * @return warning or empty string
         * @since 1.17.0
         */
        @SuppressWarnings("unused")
        @Restricted(NoExternalUse.class) // invoked from Stapler
        public FormValidation doCheckHookRegistered(@AncestorInPath Item item) {
            Preconditions.checkNotNull(item, "Item can't be null if wants to check hook in monitor");

            if (!item.hasPermission(Item.CONFIGURE)) {
                return FormValidation.ok();
            }

            Collection<GitHubRepositoryName> repos = GitHubRepositoryNameContributor.parseAssociatedNames(item);

            for (GitHubRepositoryName repo : repos) {
                if (monitor.isProblemWith(repo)) {
                    return FormValidation.warning(
                            org.jenkinsci.plugins.github.Messages.github_trigger_check_method_warning_details(
                                    repo.getUserName(), repo.getRepositoryName(), repo.getHost()
                            ));
                }
            }

            return FormValidation.ok();
        }
    }

    /**
     * Set to false to prevent the user from overriding the hook URL.
     */
    public static final boolean ALLOW_HOOKURL_OVERRIDE = !Boolean.getBoolean(
            GitHubPushTrigger.class.getName() + ".disableOverride"
    );

    private static final Logger LOGGER = LoggerFactory.getLogger(GitHubPushTrigger.class);
} public class GitHubRepositoryName {

    private static final Logger LOGGER = LoggerFactory.getLogger(GitHubRepositoryName.class);

    private static final Pattern[] URL_PATTERNS = {
            /**
             * The first set of patterns extract the host, owner and repository names
             * from URLs that include a '.git' suffix, removing the suffix from the
             * repository name.
             */
            Pattern.compile(".+@(.+):([^/]+)/([^/]+)\\.git(?:/)?"),
            Pattern.compile("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"),
            Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"),
            Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"),
            Pattern.compile("(?:git\\+)?ssh://(?:.+@)?([^/]+)/([^/]+)/([^/]+)\\.git(?:/)?"),
            /**
             * The second set of patterns extract the host, owner and repository names
             * from all other URLs. Note that these patterns must be processed *after*
             * the first set, to avoid any '.git' suffix that may be present being included
             * in the repository name.
             */
            Pattern.compile(".+@(.+):([^/]+)/([^/]+)/?"),
            Pattern.compile("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)/?"),
            Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)/?"),
            Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)/?"),
            Pattern.compile("(?:git\\+)?ssh://(?:.+@)?([^/]+)/([^/]+)/([^/]+)/?"),
    };

    /**
     * Create {@link GitHubRepositoryName} from URL
     *
     * @param url repo url. Can be null
     *
     * @return parsed {@link GitHubRepositoryName} or null if it cannot be parsed from the specified URL
     */
    @CheckForNull
    public static GitHubRepositoryName create(String url) {
        LOGGER.debug("Constructing from URL {}", url);
        for (Pattern p : URL_PATTERNS) {
            Matcher m = p.matcher(trimToEmpty(url));
            if (m.matches()) {
                LOGGER.debug("URL matches {}", m);
                GitHubRepositoryName ret = new GitHubRepositoryName(m.group(1), m.group(2), m.group(3));
                LOGGER.debug("Object is {}", ret);
                return ret;
            }
        }
        LOGGER.debug("Could not match URL {}", url);
        return null;
    }

    /**
     * @param projectProperty project property to extract url. Can be null
     *
     * @return parsed as {@link GitHubRepositoryName} object url to GitHub project
     * @see #create(String)
     * @since 1.14.1
     */
    @CheckForNull
    public static GitHubRepositoryName create(GithubProjectProperty projectProperty) {
        if (projectProperty == null) {
            return null;
        }

        return GitHubRepositoryName.create(projectProperty.getProjectUrlStr());
    }


    @SuppressWarnings("visibilitymodifier")
    public final String host;
    @SuppressWarnings("visibilitymodifier")
    public final String userName;
    @SuppressWarnings("visibilitymodifier")
    public final String repositoryName;

    public GitHubRepositoryName(String host, String userName, String repositoryName) {
        this.host = host;
        this.userName = userName;
        this.repositoryName = repositoryName;
    }

    public String getHost() {
        return host;
    }

    public String getUserName() {
        return userName;
    }

    public String getRepositoryName() {
        return repositoryName;
    }

    /**
     * Resolves this name to the actual reference by {@link GHRepository}
     *
     * Shortcut for {@link #resolve(Predicate)} with always true predicate
     * ({@link Predicates#alwaysTrue()}) as argument
     */
    public Iterable<GHRepository> resolve() {
        return resolve(Predicates.<GitHubServerConfig>alwaysTrue());
    }

    /**
     * Resolves this name to the actual reference by {@link GHRepository}.
     *
     * Since the system can store multiple credentials,
     * and only some of them might be able to see this name in question,
     * this method uses {@link org.jenkinsci.plugins.github.config.GitHubPluginConfig#findGithubConfig(Predicate)}
     * and attempt to find the right credential that can
     * access this repository.
     *
     * Any predicate as argument will be combined with {@link GitHubServerConfig#withHost(String)} to find only
     * corresponding for this repo name authenticated github repository
     *
     * This method walks multiple repositories for each credential that can access the repository. Depending on
     * what you are trying to do with the repository, you might have to keep trying until a {@link GHRepository}
     * with suitable permission is returned.
     *
     * @param predicate helps to filter only useful for resolve {@link GitHubServerConfig}s
     *
     * @return iterable with lazy login process for getting authenticated repos
     * @since 1.13.0
     */
    public Iterable<GHRepository> resolve(Predicate<GitHubServerConfig> predicate) {
        return from(GitHubPlugin.configuration().findGithubConfig(and(withHost(host), predicate)))
                .transform(toGHRepository(this))
                .filter(notNull());
    }

    /**
     * Variation of {@link #resolve()} method that just returns the first valid repository object.
     *
     * This is useful if the caller only relies on the read access to the repository and doesn't need to
     * walk possible candidates.
     */
    @CheckForNull
    public GHRepository resolveOne() {
        return from(resolve()).first().orNull();
    }

    /**
     * Does this repository match the repository referenced in the given {@link GHCommitPointer}?
     */
    public boolean matches(GHCommitPointer commit) {
        final GHUser user;
        try {
            user = commit.getUser();
        } catch (IOException ex) {
            LOGGER.debug("Failed to extract user from commit " + commit, ex);
            return false;
        }

        return userName.equals(user.getLogin())
                && repositoryName.equals(commit.getRepository().getName())
                && host.equals(commit.getRepository().getHtmlUrl().getHost());
    }

    /**
     * Does this repository match the repository referenced in the given {@link GHCommitPointer}?
     */
    public boolean matches(GHRepository repo) throws IOException {
        return userName.equals(repo.getOwner().getLogin()) // TODO: use getOwnerName
                && repositoryName.equals(repo.getName())
                && host.equals(repo.getHtmlUrl().getHost());
    }

    @Override
    public boolean equals(Object obj) {
        return EqualsBuilder.reflectionEquals(this, obj);
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(host).append(userName).append(repositoryName).build();
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, SHORT_PREFIX_STYLE)
                .append("host", host).append("username", userName).append("repository", repositoryName).build();
    }

    private static Function<GitHub, GHRepository> toGHRepository(final GitHubRepositoryName repoName) {
        return new NullSafeFunction<GitHub, GHRepository>() {
            @Override
            protected GHRepository applyNullSafe(@NonNull GitHub gitHub) {
                try {
                    return gitHub.getRepository(format("%s/%s", repoName.getUserName(), repoName.getRepositoryName()));
                } catch (IOException e) {
                    LOGGER.warn("Failed to obtain repository {}", this, e);
                    return null;
                }
            }
        };
    }
} public class GHSubscriberEvent extends SCMEvent<String> {
    /**
     * The type of event.
     */
    private final GHEvent ghEvent;

    /**
     * Constructs a new {@link GHSubscriberEvent}.
     *
     * @param origin  the origin (see {@link SCMEvent#originOf(javax.servlet.http.HttpServletRequest)}) or {@code null}.
     * @param ghEvent the type of event received from GitHub.
     * @param payload the event payload.
     */
    public GHSubscriberEvent(@CheckForNull String origin, @NonNull GHEvent ghEvent, @NonNull String payload) {
        super(Type.UPDATED, payload, origin);
        this.ghEvent = ghEvent;
    }

    /**
     * Gets the type of event received.
     *
     * @return the type of event received.
     */
    public GHEvent getGHEvent() {
        return ghEvent;
    }

} public class GitHubPlugin extends Plugin {
    /**
     * Launched before plugin starts
     * Adds alias for {@link GitHubPlugin} to simplify resulting xml.
     */
    public static void addXStreamAliases() {
        Migrator.enableCompatibilityAliases();
        Migrator.enableAliases();
    }

    /**
     * Launches migration after all extensions have been augmented as we need to ensure that the credentials plugin
     * has been initialized.
     * We need ensure that migrator will run after xstream aliases will be added.
     * @see <a href="https://issues.jenkins-ci.org/browse/JENKINS-36446">JENKINS-36446</a>
     */
    @Initializer(after = InitMilestone.EXTENSIONS_AUGMENTED, before = InitMilestone.JOB_LOADED)
    public static void runMigrator() throws Exception {
        new Migrator().migrate();
    }

    @Override
    public void start() throws Exception {
        addXStreamAliases();
    }

    /**
     * Shortcut method for getting instance of {@link GitHubPluginConfig}.
     *
     * @return configuration of plugin
     */
    @NonNull
    public static GitHubPluginConfig configuration() {
        return defaultIfNull(
                GitHubPluginConfig.all().get(GitHubPluginConfig.class),
                GitHubPluginConfig.EMPTY_CONFIG
        );
    }
} public class GitHubServerConfig extends AbstractDescribableImpl<GitHubServerConfig> {
    private static final Logger LOGGER = LoggerFactory.getLogger(GitHubServerConfig.class);

    /**
     * Common prefixes that we should remove when inferring a {@link #name}.
     *
     * @since 1.28.0
     */
    private static final String[] COMMON_PREFIX_HOSTNAMES = {
        "git.",
        "github.",
        "vcs.",
        "scm.",
        "source."
    };
    /**
     * Because of {@link GitHub} hide this const from external use we need to store it here
     */
    public static final String GITHUB_URL = "https://api.github.com";

    /**
     * The name to display for the public GitHub service.
     *
     * @since 1.28.0
     */
    private static final String PUBLIC_GITHUB_NAME = "GitHub";

    /**
     * Used as default token value if no any creds found by given credsId.
     */
    private static final String UNKNOWN_TOKEN = "UNKNOWN_TOKEN";
    /**
     * Default value in MB for client cache size
     *
     * @see #getClientCacheSize()
     */
    public static final int DEFAULT_CLIENT_CACHE_SIZE_MB = 20;

    /**
     * The optional display name of this server.
     */
    @CheckForNull
    private String name;
    private String apiUrl = GITHUB_URL;
    private boolean manageHooks = true;
    private final String credentialsId;

    /**
     * @see #getClientCacheSize()
     * @see #setClientCacheSize(int)
     */
    private int clientCacheSize = DEFAULT_CLIENT_CACHE_SIZE_MB;

    /**
     * To avoid creation of new one on every login with this config
     */
    private transient GitHub cachedClient;

    @DataBoundConstructor
    public GitHubServerConfig(String credentialsId) {
        this.credentialsId = credentialsId;
    }

    /**
     * Sets the optional display name.
     * @param name the optional display name.
     */
    @DataBoundSetter
    public void setName(@CheckForNull String name) {
        this.name = Util.fixEmptyAndTrim(name);
    }

    /**
     * Set the API endpoint.
     *
     * @param apiUrl custom url if GH. Default value will be used in case of custom is unchecked or value is blank
     */
    @DataBoundSetter
    public void setApiUrl(String apiUrl) {
        this.apiUrl = defaultIfBlank(apiUrl, GITHUB_URL);
    }

    /**
     * This server config will be used to manage GH Hooks if true
     *
     * @param manageHooks false to ignore this config on hook auto-management
     */
    @DataBoundSetter
    public void setManageHooks(boolean manageHooks) {
        this.manageHooks = manageHooks;
    }

    /**
     * This method was introduced to hide custom api url under checkbox, but now UI simplified to show url all the time
     * see jenkinsci/github-plugin/pull/112 for more details
     *
     * @param customApiUrl ignored
     *
     * @deprecated simply remove usage of this method, it ignored now. Should be removed after 20 sep 2016.
     */
    @Deprecated
    public void setCustomApiUrl(boolean customApiUrl) {
    }

    /**
     * Gets the optional display name of this server.
     *
     * @return the optional display name of this server, may be empty or {@code null} but best effort is made to ensure
     * that it has some meaningful text.
     * @since 1.28.0
     */
    public String getName() {
        return name;
    }

    /**
     * Gets the formatted display name (which will always include the api url)
     *
     * @return the formatted display name.
     * @since 1.28.0
     */
    public String getDisplayName() {
        String gitHubName = getName();
        boolean isGitHubCom = StringUtils.isBlank(apiUrl) || GITHUB_URL.equals(apiUrl);
        if (StringUtils.isBlank(gitHubName)) {
            gitHubName = isGitHubCom ? PUBLIC_GITHUB_NAME : SCMName.fromUrl(apiUrl, COMMON_PREFIX_HOSTNAMES);
        }
        String gitHubUrl = isGitHubCom ? "https://github.com" : StringUtils.removeEnd(apiUrl, "/api/v3");
        return StringUtils.isBlank(gitHubName)
                ? gitHubUrl
                : Messages.GitHubServerConfig_displayName(gitHubName, gitHubUrl);
    }

    public String getApiUrl() {
        return apiUrl;
    }

    public boolean isManageHooks() {
        return manageHooks;
    }

    public String getCredentialsId() {
        return credentialsId;
    }

    /**
     * Capacity of cache for GitHub client in MB.
     *
     * Defaults to 20 MB
     *
     * @since 1.14.0
     */
    public int getClientCacheSize() {
        return clientCacheSize;
    }

    /**
     * @param clientCacheSize capacity of cache for GitHub client in MB, set to <= 0 to turn off this feature
     */
    @DataBoundSetter
    public void setClientCacheSize(int clientCacheSize) {
        this.clientCacheSize = clientCacheSize;
    }

    /**
     * @return cached GH client or null
     */
    protected synchronized GitHub getCachedClient() {
        return cachedClient;
    }

    /**
     * Used by {@link org.jenkinsci.plugins.github.config.GitHubServerConfig.ClientCacheFunction}
     *
     * @param cachedClient updated client. Maybe null to invalidate cache
     */
    protected synchronized void setCachedClient(GitHub cachedClient) {
        this.cachedClient = cachedClient;
    }

    /**
     * Checks GH url for equality to default api url
     *
     * @param apiUrl should be not blank and not equal to default url to return true
     *
     * @return true if url not blank and not equal to default
     */
    public static boolean isUrlCustom(String apiUrl) {
        return isNotBlank(apiUrl) && !GITHUB_URL.equals(apiUrl);
    }

    /**
     * Converts server config to authorized GH instance. If login process is not successful it returns null
     *
     * @return function to convert config to gh instance
     * @see org.jenkinsci.plugins.github.config.GitHubServerConfig.ClientCacheFunction
     */
    @CheckForNull
    public static Function<GitHubServerConfig, GitHub> loginToGithub() {
        return new ClientCacheFunction();
    }

    /**
     * Extracts token from secret found by {@link #secretFor(String)}
     * Returns {@link #UNKNOWN_TOKEN} if no any creds secret found with this id.
     *
     * @param credentialsId id to find creds
     *
     * @return token from creds or default non empty string
     */
    @NonNull
    public static String tokenFor(String credentialsId) {
        return secretFor(credentialsId).or(new Supplier<Secret>() {
            @Override
            public Secret get() {
                return Secret.fromString(UNKNOWN_TOKEN);
            }
        }).getPlainText();
    }

    /**
     * Tries to find {@link StringCredentials} by id and returns secret from it.
     *
     * @param credentialsId id to find creds
     *
     * @return secret from creds or empty optional
     */
    @NonNull
    public static Optional<Secret> secretFor(String credentialsId) {
        List<StringCredentials> creds = filter(
                lookupCredentials(StringCredentials.class,
                        Jenkins.getInstance(), ACL.SYSTEM,
                        Collections.<DomainRequirement>emptyList()),
                withId(trimToEmpty(credentialsId))
        );

        return FluentIterableWrapper.from(creds)
                .transform(new NullSafeFunction<StringCredentials, Secret>() {
                    @Override
                    protected Secret applyNullSafe(@NonNull StringCredentials input) {
                        return input.getSecret();
                    }
                }).first();
    }

    /**
     * Returns true if given host is part of stored (or default if blank) api url
     *
     * For example:
     * withHost(api.github.com).apply(config for ~empty~) = true
     * withHost(api.github.com).apply(config for api.github.com) = true
     * withHost(api.github.com).apply(config for github.company.com) = false
     *
     * @param host host to find in api url
     *
     * @return predicate to match against {@link GitHubServerConfig}
     */
    public static Predicate<GitHubServerConfig> withHost(final String host) {
        return new NullSafePredicate<GitHubServerConfig>() {
            @Override
            protected boolean applyNullSafe(@NonNull GitHubServerConfig github) {
                return defaultIfEmpty(github.getApiUrl(), GITHUB_URL).contains(host);
            }
        };
    }

    /**
     * Returns true if config can be used in hooks managing
     *
     * @return predicate to match against {@link GitHubServerConfig}
     */
    public static Predicate<GitHubServerConfig> allowedToManageHooks() {
        return new NullSafePredicate<GitHubServerConfig>() {
            @Override
            protected boolean applyNullSafe(@NonNull GitHubServerConfig github) {
                return github.isManageHooks();
            }
        };
    }

    @Extension
    public static class DescriptorImpl extends Descriptor<GitHubServerConfig> {

        @Override
        public String getDisplayName() {
            return "GitHub Server";
        }

        @SuppressWarnings("unused")
        public ListBoxModel doFillCredentialsIdItems(@QueryParameter String apiUrl,
                                                     @QueryParameter String credentialsId) {
            if (!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) {
                return new StandardListBoxModel().includeCurrentValue(credentialsId);
            }
            return new StandardListBoxModel()
                    .includeEmptyValue()
                    .includeMatchingAs(ACL.SYSTEM,
                            Jenkins.getInstance(),
                            StringCredentials.class,
                            fromUri(defaultIfBlank(apiUrl, GITHUB_URL)).build(),
                            CredentialsMatchers.always()
                    );
        }

        @RequirePOST
        @Restricted(DoNotUse.class) // WebOnly
        @SuppressWarnings("unused")
        public FormValidation doVerifyCredentials(
                @QueryParameter String apiUrl,
                @QueryParameter String credentialsId) throws IOException {
            Jenkins.getActiveInstance().checkPermission(Jenkins.ADMINISTER);

            GitHubServerConfig config = new GitHubServerConfig(credentialsId);
            config.setApiUrl(apiUrl);
            config.setClientCacheSize(0);
            GitHub gitHub = new GitHubLoginFunction().apply(config);

            try {
                if (gitHub != null && gitHub.isCredentialValid()) {
                    return FormValidation.ok("Credentials verified for user %s, rate limit: %s",
                            gitHub.getMyself().getLogin(), gitHub.getRateLimit().remaining);
                } else {
                    return FormValidation.error("Failed to validate the account");
                }
            } catch (IOException e) {
                return FormValidation.error(e, "Failed to validate the account");
            }
        }

        @SuppressWarnings("unused")
        public FormValidation doCheckApiUrl(@QueryParameter String value) {
            try {
                new URL(value);
            } catch (MalformedURLException e) {
                return FormValidation.error("Malformed GitHub url (%s)", e.getMessage());
            }

            if (GITHUB_URL.equals(value)) {
                return FormValidation.ok();
            }

            if (value.endsWith("/api/v3") || value.endsWith("/api/v3/")) {
                return FormValidation.ok();
            }

            return FormValidation.warning("GitHub Enterprise API URL ends with \"/api/v3\"");
        }
    }

    /**
     * Function to get authorized GH client and cache it in config
     * has {@link #loginToGithub()} static factory
     */
    private static class ClientCacheFunction extends NullSafeFunction<GitHubServerConfig, GitHub> {
        @Override
        protected GitHub applyNullSafe(@NonNull GitHubServerConfig github) {
            if (github.getCachedClient() == null) {
                github.setCachedClient(new GitHubLoginFunction().apply(github));
            }
            return github.getCachedClient();
        }
    }
} public abstract class GHEventsSubscriber implements ExtensionPoint {
    private static final Logger LOGGER = LoggerFactory.getLogger(GHEventsSubscriber.class);
    @CheckForNull
    private transient Boolean hasIsApplicableItem;

    /**
     * Should return true only if this subscriber interested in {@link #events()} set for this project
     * Don't call it directly, use {@link #isApplicableFor} static function
     *
     * @param project to check
     *
     * @return {@code true} to provide events to register and subscribe for this project
     * @deprecated override {@link #isApplicable(Item)} instead.
     */
    @Deprecated
    protected boolean isApplicable(@Nullable Job<?, ?> project) {
        if (checkIsApplicableItem()) {
            return isApplicable((Item) project);
        }
        // a legacy implementation which should not have been calling super.isApplicable(Job)
        throw new AbstractMethodError("you must override the new overload of isApplicable");
    }

    /**
     * Should return true only if this subscriber interested in {@link #events()} set for this project
     * Don't call it directly, use {@link #isApplicableFor} static function
     *
     * @param item to check
     *
     * @return {@code true} to provide events to register and subscribe for this item
     * @since 1.25.0
     */
    protected abstract boolean isApplicable(@Nullable Item item);

    /**
     * Call {@link #isApplicable(Item)} with safety for calling to legacy implementations before the abstract method
     * was switched from {@link #isApplicable(Job)}.
     * @param item to check.
     * @return {@code true} to provide events to register and subscribe for this item
     */
    @SuppressWarnings("deprecation")
    private boolean safeIsApplicable(@Nullable Item item) {
        return checkIsApplicableItem() ? isApplicable(item) : item instanceof Job && isApplicable((Job<?, ?>) item);
    }

    private boolean checkIsApplicableItem() {
        if (hasIsApplicableItem == null) {
            boolean implemented = false;
            // cannot use Util.isOverridden because method is protected and isOverridden only checks public methods
            Class<?> clazz = getClass();
            while (clazz != null && clazz != GHEventsSubscriber.class) {
                try {
                    Method isApplicable = clazz.getDeclaredMethod("isApplicable", Item.class);
                    if (isApplicable.getDeclaringClass() != GHEventsSubscriber.class) {
                        // ok this is the first method we have found that could be an override
                        // if somebody overrode an inherited method with and `abstract` then we don't have the method
                        implemented = !Modifier.isAbstract(isApplicable.getModifiers());
                        break;
                    }
                } catch (NoSuchMethodException e) {
                    clazz = clazz.getSuperclass();
                }
            }
            // idempotent so no need for synchronization
            this.hasIsApplicableItem = implemented;
        }
        return hasIsApplicableItem;
    }

    /**
     * Should be not null. Should return only events which this extension can parse in {@link #onEvent(GHEvent, String)}
     * Don't call it directly, use {@link #extractEvents()} or {@link #isInterestedIn(GHEvent)} static functions
     *
     * @return immutable set of events this subscriber wants to register and then subscribe to.
     */
    protected abstract Set<GHEvent> events();

    /**
     * This method called when root action receives webhook from GH and this extension is interested in such
     * events (provided by {@link #events()} method). By default do nothing and can be overridden to implement any
     * parse logic
     * Don't call it directly, use {@link #processEvent(GHSubscriberEvent)} static function
     *
     * @param event   gh-event (as of PUSH, ISSUE...). One of returned by {@link #events()} method. Never null.
     * @param payload payload of gh-event. Never blank. Can be parsed with help of GitHub#parseEventPayload
     * @deprecated override {@link #onEvent(GHSubscriberEvent)} instead.
     */
    @Deprecated
    protected void onEvent(GHEvent event, String payload) {
        // do nothing by default
    }

    /**
     * This method called when root action receives webhook from GH and this extension is interested in such
     * events (provided by {@link #events()} method). By default do nothing and can be overridden to implement any
     * parse logic
     * Don't call it directly, use {@link #processEvent(GHSubscriberEvent)} static function
     *
     * @param event   the event.
     * @since 1.26.0
     */
    protected void onEvent(GHSubscriberEvent event) {
        onEvent(event.getGHEvent(), event.getPayload());
    }

    /**
     * @return All subscriber extensions
     */
    public static ExtensionList<GHEventsSubscriber> all() {
        return Jenkins.getInstance().getExtensionList(GHEventsSubscriber.class);
    }

    /**
     * Converts each subscriber to set of GHEvents
     *
     * @return converter to use in iterable manipulations
     */
    public static Function<GHEventsSubscriber, Set<GHEvent>> extractEvents() {
        return new NullSafeFunction<GHEventsSubscriber, Set<GHEvent>>() {
            @Override
            protected Set<GHEvent> applyNullSafe(@NonNull GHEventsSubscriber subscriber) {
                return defaultIfNull(subscriber.events(), Collections.<GHEvent>emptySet());
            }
        };
    }

    /**
     * Helps to filter only GHEventsSubscribers that can return TRUE on given project
     *
     * @param project to check every GHEventsSubscriber for being applicable
     *
     * @return predicate to use in iterable filtering
     * @see #isApplicable
     * @deprecated use {@link #isApplicableFor(Item)}.
     */
    @Deprecated
    public static Predicate<GHEventsSubscriber> isApplicableFor(final Job<?, ?> project) {
        return isApplicableFor((Item) project);
    }

    /**
     * Helps to filter only GHEventsSubscribers that can return TRUE on given item
     *
     * @param item to check every GHEventsSubscriber for being applicable
     *
     * @return predicate to use in iterable filtering
     * @see #isApplicable
     * @since 1.25.0
     */
    public static Predicate<GHEventsSubscriber> isApplicableFor(final Item item) {
        return new NullSafePredicate<GHEventsSubscriber>() {
            @Override
            protected boolean applyNullSafe(@NonNull GHEventsSubscriber subscriber) {
                return subscriber.safeIsApplicable(item);
            }
        };
    }

    /**
     * Predicate which returns true on apply if current subscriber is interested in event
     *
     * @param event should be one of {@link #events()} set to return true on apply
     *
     * @return predicate to match against {@link GHEventsSubscriber}
     */
    public static Predicate<GHEventsSubscriber> isInterestedIn(final GHEvent event) {
        return new NullSafePredicate<GHEventsSubscriber>() {
            @Override
            protected boolean applyNullSafe(@NonNull GHEventsSubscriber subscriber) {
                return defaultIfNull(subscriber.events(), emptySet()).contains(event);
            }
        };
    }

    /**
     * Function which calls {@link #onEvent(GHSubscriberEvent)} for every subscriber on apply
     *
     * @param event   from hook. Applied only with event from {@link #events()} set
     * @param payload string content of hook from GH. Never blank
     *
     * @return function to process {@link GHEventsSubscriber} list. Returns null on apply.
     * @deprecated use {@link #processEvent(GHSubscriberEvent)}
     */
    @Deprecated
    public static Function<GHEventsSubscriber, Void> processEvent(final GHEvent event, final String payload) {
        return processEvent(new GHSubscriberEvent(SCMEvent.originOf(Stapler.getCurrentRequest()), event, payload));
    }

    /**
     * Function which calls {@link #onEvent(GHSubscriberEvent)} for every subscriber on apply
     *
     * @param event the event
     *
     * @return function to process {@link GHEventsSubscriber} list. Returns null on apply.
     * @since 1.26.0
     */
    public static Function<GHEventsSubscriber, Void> processEvent(final GHSubscriberEvent event) {
        return new NullSafeFunction<GHEventsSubscriber, Void>() {
            @Override
            protected Void applyNullSafe(@NonNull GHEventsSubscriber subscriber) {
                try {
                    subscriber.onEvent(event);
                } catch (Throwable t) {
                    LOGGER.error("Subscriber {} failed to process {} hook, skipping...",
                            subscriber.getClass().getName(), event, t);
                }
                return null;
            }
        };
    }
} public class WebhookManager {
    private static final Logger LOGGER = LoggerFactory.getLogger(WebhookManager.class);

    private final URL endpoint;

    /**
     * Use {@link #forHookUrl(URL)} to create new one
     *
     * @param endpoint url which will be created as hook on GH
     */
    protected WebhookManager(URL endpoint) {
        this.endpoint = endpoint;
    }

    /**
     * @see #WebhookManager(URL)
     */
    public static WebhookManager forHookUrl(URL endpoint) {
        return new WebhookManager(endpoint);
    }

    /**
     * Creates runnable with ability to create hooks for given project
     * For each GH repo name contributed by {@link com.cloudbees.jenkins.GitHubRepositoryNameContributor},
     * this runnable creates hook (with clean old one).
     *
     * Hook events job interested in, contributes to full set instances of {@link GHEventsSubscriber}.
     * New events will be merged with old ones from existent hook.
     *
     * By default only push event is registered
     *
     * @param project to find for which repos we should create hooks
     *
     * @return runnable to create hooks on run
     * @see #createHookSubscribedTo(List)
     * @deprecated use {@link #registerFor(Item)}
     */
    @Deprecated
    public Runnable registerFor(final Job<?, ?> project) {
        return registerFor((Item) project);
    }

    /**
     * Creates runnable with ability to create hooks for given project
     * For each GH repo name contributed by {@link com.cloudbees.jenkins.GitHubRepositoryNameContributor},
     * this runnable creates hook (with clean old one).
     *
     * Hook events job interested in, contributes to full set instances of {@link GHEventsSubscriber}.
     * New events will be merged with old ones from existent hook.
     *
     * By default only push event is registered
     *
     * @param item to find for which repos we should create hooks
     *
     * @return runnable to create hooks on run
     * @see #createHookSubscribedTo(List)
     * @since 1.25.0
     */
    public Runnable registerFor(final Item item) {
        final Collection<GitHubRepositoryName> names = parseAssociatedNames(item);

        final List<GHEvent> events = from(GHEventsSubscriber.all())
                .filter(isApplicableFor(item))
                .transformAndConcat(extractEvents()).toList();

        return new Runnable() {
            public void run() {
                if (events.isEmpty()) {
                    LOGGER.debug("No any subscriber interested in {}, but hooks creation launched, skipping...",
                            item.getFullName());
                    return;
                }

                LOGGER.info("GitHub webhooks activated for job {} with {} (events: {})",
                        item.getFullName(), names, events);

                from(names)
                        .transform(createHookSubscribedTo(events))
                        .filter(notNull())
                        .filter(log("Created hook")).toList();
            }
        };
    }

    /**
     * Used to cleanup old hooks in case of removed or reconfigured trigger
     * since JENKINS-28138 this method permanently removes service hooks
     *
     * So if the trigger for given name was only reconfigured, this method filters only service hooks
     * (with help of aliveRepos names list), otherwise this method removes all hooks for managed url
     *
     * @param name       repository to clean hooks
     * @param aliveRepos repository list which has enabled trigger in jobs
     */
    public void unregisterFor(GitHubRepositoryName name, List<GitHubRepositoryName> aliveRepos) {
        try {
            GHRepository repo = repoWithWebhookAccess(name);
            if (repo == null) {
                return;
            }

            LOGGER.debug("Check {} for redundant hooks...", repo);

            Predicate<GHHook> predicate = aliveRepos.contains(name)
                    ? serviceWebhookFor(endpoint) // permanently clear service hooks (JENKINS-28138)
                    : or(serviceWebhookFor(endpoint), webhookFor(endpoint));

            from(fetchHooks().apply(repo))
                    .filter(predicate)
                    .filter(deleteWebhook())
                    .filter(log("Deleted hook")).toList();

        } catch (Throwable t) {
            LOGGER.warn("Failed to remove hook from {}", name, t);
            GitHubHookRegisterProblemMonitor.get().registerProblem(name, t);
        }
    }

    private GHRepository repoWithWebhookAccess(GitHubRepositoryName name) {
        FluentIterableWrapper<GHRepository> reposAllowedtoManageWebhooks = from(name.resolve(allowedToManageHooks()));
        if (!reposAllowedtoManageWebhooks.first().isPresent()) {
            LOGGER.debug("There are no github repos configured to allow webhook management for: {}", name);
            return null;
        }
        com.google.common.base.Optional<GHRepository> repoWithAdminAccess = reposAllowedtoManageWebhooks
                .firstMatch(withAdminAccess());
        if (!repoWithAdminAccess.isPresent()) {
            LOGGER.debug("None of the github repos configured have admin access for: {}", name);
            return null;
        }
        GHRepository repo = repoWithAdminAccess.get();
        return repo;
    }

    /**
     * Main logic of {@link #registerFor(Item)}.
     * Updates hooks with replacing old ones with merged new ones
     *
     * @param events calculated events list to be registered in hook
     *
     * @return function to register hooks for given events
     */
    protected Function<GitHubRepositoryName, GHHook> createHookSubscribedTo(final List<GHEvent> events) {
        return new NullSafeFunction<GitHubRepositoryName, GHHook>() {
            @Override
            protected GHHook applyNullSafe(@NonNull GitHubRepositoryName name) {
                try {
                    GHRepository repo = repoWithWebhookAccess(name);
                    if (repo == null) {
                        return null;
                    }

                    Validate.notEmpty(events, "Events list for hook can't be empty");

                    Set<GHHook> hooks = from(fetchHooks().apply(repo))
                            .filter(webhookFor(endpoint))
                            .toSet();

                    Set<GHEvent> alreadyRegistered = from(hooks)
                            .transformAndConcat(eventsFromHook()).toSet();

                    if (hooks.size() == 1 && alreadyRegistered.containsAll(events)) {
                        LOGGER.debug("Hook already registered for events {}", events);
                        return null;
                    }

                    Set<GHEvent> merged = from(alreadyRegistered).append(events).toSet();

                    from(hooks)
                            .filter(deleteWebhook())
                            .filter(log("Replaced hook")).toList();

                    return createWebhook(endpoint, merged).apply(repo);
                } catch (Exception e) {
                    LOGGER.warn("Failed to add GitHub webhook for {}", name, e);
                    GitHubHookRegisterProblemMonitor.get().registerProblem(name, e);
                }
                return null;
            }
        };
    }

    /**
     * Mostly debug method. Logs hook manipulation result
     *
     * @param format prepended comment for log
     *
     * @return always true predicate
     */
    protected Predicate<GHHook> log(final String format) {
        return new NullSafePredicate<GHHook>() {
            @Override
            protected boolean applyNullSafe(@NonNull GHHook input) {
                LOGGER.debug(format("%s {} (events: {})", format), input.getUrl(), input.getEvents());
                return true;
            }
        };
    }

    /**
     * Filters repos with admin rights (to manage hooks)
     *
     * @return true if we have admin rights for repo
     */
    protected Predicate<GHRepository> withAdminAccess() {
        return new NullSafePredicate<GHRepository>() {
            @Override
            protected boolean applyNullSafe(@NonNull GHRepository repo) {
                return repo.hasAdminAccess();
            }
        };
    }

    /**
     * Finds "Jenkins (GitHub)" service webhook
     *
     * @param url jenkins endpoint url
     *
     * @return true if hook is service hook
     */
    protected Predicate<GHHook> serviceWebhookFor(final URL url) {
        return new NullSafePredicate<GHHook>() {
            protected boolean applyNullSafe(@NonNull GHHook hook) {
                return hook.getName().equals("jenkins")
                        && hook.getConfig().get("jenkins_hook_url").equals(url.toExternalForm());
            }
        };
    }

    /**
     * Finds hook with endpoint url
     *
     * @param url jenkins endpoint url
     *
     * @return true if hook is standard webhook
     */
    protected Predicate<GHHook> webhookFor(final URL url) {
        return new NullSafePredicate<GHHook>() {
            protected boolean applyNullSafe(@NonNull GHHook hook) {
                return hook.getName().equals("web")
                        && hook.getConfig().get("url").equals(url.toExternalForm());
            }
        };
    }

    /**
     * @return converter to extract events from each hook
     */
    protected Function<GHHook, Iterable<GHEvent>> eventsFromHook() {
        return new NullSafeFunction<GHHook, Iterable<GHEvent>>() {
            @Override
            protected Iterable<GHEvent> applyNullSafe(@NonNull GHHook input) {
                return input.getEvents();
            }
        };
    }

    /*
     * ACTIONS
     */

    /**
     * @return converter to fetch from GH hooks list for each repo
     */
    protected Function<GHRepository, List<GHHook>> fetchHooks() {
        return new NullSafeFunction<GHRepository, List<GHHook>>() {
            @Override
            protected List<GHHook> applyNullSafe(@NonNull GHRepository repo) {
                try {
                    return repo.getHooks();
                } catch (IOException e) {
                    throw new GHException("Failed to fetch post-commit hooks", e);
                }
            }
        };
    }

    /**
     * @param url    jenkins endpoint url
     * @param events list of GH events jenkins interested in
     *
     * @return converter to create GH hook for given url with given events
     */
    protected Function<GHRepository, GHHook> createWebhook(final URL url, final Set<GHEvent> events) {
        return new NullSafeFunction<GHRepository, GHHook>() {
            protected GHHook applyNullSafe(@NonNull GHRepository repo) {
                try {
                    final HashMap<String, String> config = new HashMap<>();
                    config.put("url", url.toExternalForm());
                    config.put("content_type", "json");

                    // We need to pick a secret to use, so use the first one defined.
                    final Optional<Secret> secret = GitHubPlugin.configuration().getHookSecretConfigs().stream().
                            map(HookSecretConfig::getHookSecret).filter(Objects::nonNull).findFirst();

                    if (secret.isPresent()) {
                        config.put("secret", secret.get().getPlainText());
                    }

                    return repo.createHook("web", config, events, true);
                } catch (IOException e) {
                    throw new GHException("Failed to create hook", e);
                }
            }
        };
    }

    /**
     * @return annihilator for hook, returns true if deletion was successful
     */
    protected Predicate<GHHook> deleteWebhook() {
        return new NullSafePredicate<GHHook>() {
            protected boolean applyNullSafe(@NonNull GHHook hook) {
                try {
                    hook.delete();
                    return true;
                } catch (IOException e) {
                    throw new GHException("Failed to delete post-commit hook", e);
                }
            }
        };
    }
} @RunWith(MockitoJUnitRunner.class)
public class WebhookManagerTest {

    public static final GitSCM GIT_SCM = new GitSCM("ssh://git@github.com/dummy/dummy.git");
    public static final URL HOOK_ENDPOINT = endpoint("http://hook.endpoint/");
    public static final URL ANOTHER_HOOK_ENDPOINT = endpoint("http://another.url/");

    @Rule
    public JenkinsRule jenkins = new JenkinsRule();

    @Spy
    private WebhookManager manager = forHookUrl(HOOK_ENDPOINT);

    @Spy
    private GitHubRepositoryName nonactive = new GitHubRepositoryName("github.com", "dummy", "dummy");

    @Spy
    private GitHubRepositoryName active = new GitHubRepositoryName("github.com", "dummy", "active");

    @Mock
    private GHRepository repo;


    @Test
    public void shouldDoNothingOnNoAdminRights() throws Exception {
        manager.unregisterFor(nonactive, newArrayList(active));
        verify(manager, never()).withAdminAccess();
        verify(manager, never()).fetchHooks();
    }

    @Test
    public void shouldSearchBothWebAndServiceHookOnNonActiveName() throws Exception {
        doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class));
        when(repo.hasAdminAccess()).thenReturn(true);

        manager.unregisterFor(nonactive, newArrayList(active));

        verify(manager, times(1)).serviceWebhookFor(HOOK_ENDPOINT);
        verify(manager, times(1)).webhookFor(HOOK_ENDPOINT);
        verify(manager, times(1)).fetchHooks();
    }

    @Test
    public void shouldSearchOnlyServiceHookOnActiveName() throws Exception {
        doReturn(newArrayList(repo)).when(active).resolve(any(Predicate.class));
        when(repo.hasAdminAccess()).thenReturn(true);

        manager.unregisterFor(active, newArrayList(active));

        verify(manager, times(1)).serviceWebhookFor(HOOK_ENDPOINT);
        verify(manager, never()).webhookFor(HOOK_ENDPOINT);
        verify(manager, times(1)).fetchHooks();
    }

    @Test
    @WithoutJenkins
    public void shouldMatchAdminAccessWhenTrue() throws Exception {
        when(repo.hasAdminAccess()).thenReturn(true);

        assertThat("has admin access", manager.withAdminAccess().apply(repo), is(true));
    }

    @Test
    @WithoutJenkins
    public void shouldMatchAdminAccessWhenFalse() throws Exception {
        when(repo.hasAdminAccess()).thenReturn(false);

        assertThat("has no admin access", manager.withAdminAccess().apply(repo), is(false));
    }

    @Test
    @WithoutJenkins
    public void shouldMatchWebHook() {
        when(repo.hasAdminAccess()).thenReturn(false);

        GHHook hook = hook(HOOK_ENDPOINT, PUSH);

        assertThat("webhook has web name and url prop", manager.webhookFor(HOOK_ENDPOINT).apply(hook), is(true));
    }

    @Test
    @WithoutJenkins
    public void shouldNotMatchOtherUrlWebHook() {
        when(repo.hasAdminAccess()).thenReturn(false);

        GHHook hook = hook(ANOTHER_HOOK_ENDPOINT, PUSH);

        assertThat("webhook has web name and another url prop",
                manager.webhookFor(HOOK_ENDPOINT).apply(hook), is(false));
    }

    @Test
    public void shouldMergeEventsOnRegisterNewAndDeleteOldOnes() throws IOException {
        doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class));
        when(repo.hasAdminAccess()).thenReturn(true);
        Predicate<GHHook> del = spy(Predicate.class);
        when(manager.deleteWebhook()).thenReturn(del);

        GHHook hook = hook(HOOK_ENDPOINT, CREATE);
        GHHook prhook = hook(HOOK_ENDPOINT, PULL_REQUEST);
        when(repo.getHooks()).thenReturn(newArrayList(hook, prhook));

        manager.createHookSubscribedTo(copyOf(newArrayList(PUSH))).apply(nonactive);
        verify(del, times(2)).apply(any(GHHook.class));
        verify(manager).createWebhook(HOOK_ENDPOINT, EnumSet.copyOf(newArrayList(CREATE, PULL_REQUEST, PUSH)));
    }

    @Test
    public void shouldNotReplaceAlreadyRegisteredHook() throws IOException {
        doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class));
        when(repo.hasAdminAccess()).thenReturn(true);

        GHHook hook = hook(HOOK_ENDPOINT, PUSH);
        when(repo.getHooks()).thenReturn(newArrayList(hook));

        manager.createHookSubscribedTo(copyOf(newArrayList(PUSH))).apply(nonactive);
        verify(manager, never()).deleteWebhook();
        verify(manager, never()).createWebhook(any(URL.class), anySetOf(GHEvent.class));
    }

    @Test
    @Issue( "JENKINS-62116" )
    public void shouldNotReplaceAlreadyRegisteredHookWithMoreEvents() throws IOException {
        doReturn(newArrayList(repo)).when(nonactive).resolve(any(Predicate.class));
        when(repo.hasAdminAccess()).thenReturn(true);

        GHHook hook = hook(HOOK_ENDPOINT, PUSH, CREATE);
        when(repo.getHooks()).thenReturn(newArrayList(hook));

        manager.createHookSubscribedTo(copyOf(newArrayList(PUSH))).apply(nonactive);
        verify(manager, never()).deleteWebhook();
        verify(manager, never()).createWebhook(any(URL.class), anySetOf(GHEvent.class));
    }


    @Test
    public void shouldNotAddPushEventByDefaultForProjectWithoutTrigger() throws IOException {
        FreeStyleProject project = jenkins.createFreeStyleProject();
        project.setScm(GIT_SCM);

        manager.registerFor((Item)project).run();
        verify(manager, never()).createHookSubscribedTo(anyListOf(GHEvent.class));
    }

    @Test
    public void shouldAddPushEventByDefault() throws IOException {
        FreeStyleProject project = jenkins.createFreeStyleProject();
        project.addTrigger(new GitHubPushTrigger());
        project.setScm(GIT_SCM);

        manager.registerFor((Item)project).run();
        verify(manager).createHookSubscribedTo(newArrayList(PUSH));
    }

    @Test
    public void shouldReturnNullOnGettingEmptyEventsListToSubscribe() throws IOException {
        doReturn(newArrayList(repo)).when(active).resolve(any(Predicate.class));
        when(repo.hasAdminAccess()).thenReturn(true);

        assertThat("empty events list not allowed to be registered",
                forHookUrl(HOOK_ENDPOINT)
                        .createHookSubscribedTo(Collections.<GHEvent>emptyList()).apply(active), nullValue());
    }

    @Test
    public void shouldSelectOnlyHookManagedCreds() {
        GitHubServerConfig conf = new GitHubServerConfig("");
        conf.setManageHooks(false);
        GitHubPlugin.configuration().getConfigs().add(conf);

        assertThat(forHookUrl(HOOK_ENDPOINT).createHookSubscribedTo(Lists.newArrayList(PUSH))
                .apply(new GitHubRepositoryName("github.com", "name", "repo")), nullValue());
    }

    @Test
    public void shouldNotSelectCredsWithCustomHost() {
        GitHubServerConfig conf = new GitHubServerConfig("");
        conf.setApiUrl(ANOTHER_HOOK_ENDPOINT.toString());
        conf.setManageHooks(false);
        GitHubPlugin.configuration().getConfigs().add(conf);

        assertThat(forHookUrl(HOOK_ENDPOINT).createHookSubscribedTo(Lists.newArrayList(PUSH))
                .apply(new GitHubRepositoryName("github.com", "name", "repo")), nullValue());
    }

    @Test
    public void shouldSendSecretIfDefined() throws Exception {
        String secretText = "secret_text";

        storeSecretIn(GitHubPlugin.configuration(), secretText);

        manager.createWebhook(HOOK_ENDPOINT, ImmutableSet.of(PUSH)).apply(repo);

        verify(repo).createHook(
                anyString(),
                (Map<String, String>) argThat(hasEntry("secret", secretText)),
                anySetOf(GHEvent.class),
                anyBoolean()
        );

    }

    private GHHook hook(URL endpoint, GHEvent event, GHEvent... events) {
        GHHook hook = mock(GHHook.class);
        when(hook.getName()).thenReturn("web");
        when(hook.getConfig()).thenReturn(ImmutableMap.of("url", endpoint.toExternalForm()));
        when(hook.getEvents()).thenReturn(EnumSet.copyOf(asList(event, events)));
        return hook;
    }

    private static URL endpoint(String endpoint) {
        try {
            return new URL(endpoint);
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
    }
} @CheckForNull
public static GitHubRepositoryName create(String url) {
    LOGGER.debug("Constructing from URL {}", url);
    for (Pattern p : URL_PATTERNS) {
        Matcher m = p.matcher(trimToEmpty(url));
        if (m.matches()) {
            LOGGER.debug("URL matches {}", m);
            GitHubRepositoryName ret = new GitHubRepositoryName(m.group(1), m.group(2), m.group(3));
            LOGGER.debug("Object is {}", ret);
            return ret;
        }
    }
    LOGGER.debug("Could not match URL {}", url);
    return null;
} public static String classpath(String path) {
    return classpath(GitHubWebHookFullTest.class, path);
}
import com.cloudbees.jenkins.GitHubPushTrigger;
import com.cloudbees.jenkins.GitHubRepositoryName;
import hudson.model.FreeStyleProject;
import hudson.model.Item;
import hudson.plugins.git.GitSCM;
import org.jenkinsci.plugins.github.extension.GHSubscriberEvent;
import org.jenkinsci.plugins.github.GitHubPlugin;
import org.jenkinsci.plugins.github.config.GitHubServerConfig;
import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
import org.jenkinsci.plugins.github.webhook.WebhookManager;
import org.jenkinsci.plugins.github.webhook.WebhookManagerTest;
import org.jenkinsci.plugins.github.webhook.subscriber.PingGHEventSubscriber;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.recipes.LocalData;
import org.kohsuke.github.GHEvent;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import javax.inject.Inject;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import static com.cloudbees.jenkins.GitHubRepositoryName.create;
import static com.cloudbees.jenkins.GitHubWebHookFullTest.classpath;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.when;
package org.jenkinsci.plugins.github.admin;




/**
 * @author lanwen (Merkushev Kirill)
 */
@Issue("JENKINS-24690")
@RunWith(MockitoJUnitRunner.class)
public class GitHubHookRegisterProblemMonitorTest {
    private static final GitHubRepositoryName REPO = new GitHubRepositoryName("host", "user", "repo");
    private static final String REPO_GIT_URI = "host/user/repo.git";
    private static final GitSCM REPO_GIT_SCM = new GitSCM("git://"+REPO_GIT_URI);

    private static final GitHubRepositoryName REPO_FROM_PING_PAYLOAD = create("https://github.com/lanwen/test");

    @Inject
    private GitHubHookRegisterProblemMonitor monitor;

    @Inject
    private GitHubHookRegisterProblemMonitor.GitHubHookRegisterProblemManagementLink link;

    @Inject
    private PingGHEventSubscriber pingSubscr;

    @Rule
    public JenkinsRule jRule = new JenkinsRule();

    @Mock
    private GitHub github;
    @Mock
    private GHRepository ghRepository;

    class GitHubServerConfigForTest extends GitHubServerConfig {
        public GitHubServerConfigForTest(String credentialsId) {
            super(credentialsId);
            this.setCachedClient(github);
        }
    }

    @Before
    public void setUp() throws Exception {
        jRule.getInstance().getInjector().injectMembers(this);
        GitHubServerConfig config = new GitHubServerConfigForTest("");
        config.setApiUrl("http://" + REPO_GIT_URI);
        GitHubPlugin.configuration().setConfigs(Arrays.asList(config));
        when(github.getRepository("user/repo")).thenReturn(ghRepository);
        when(ghRepository.hasAdminAccess()).thenReturn(true);
    }

    @Test
    public void shouldRegisterProblem() throws Exception {
        monitor.registerProblem(REPO, new IOException());
        assertThat("should register problem", monitor.isProblemWith(REPO), is(true));
    }

    @Test
    public void shouldResolveProblem() throws Exception {
        monitor.registerProblem(REPO, new IOException());
        monitor.resolveProblem(REPO);

        assertThat("should be no problem", monitor.isProblemWith(REPO), is(false));
    }

    @Test
    public void shouldNotAddNullRepo() throws Exception {
        monitor.registerProblem(null, new IOException());
        assertThat("should be no problems", monitor.getProblems().keySet(), empty());
    }

    @Test
    public void shouldNotAddNullExc() throws Exception {
        monitor.registerProblem(REPO, null);
        assertThat("should be no problems", monitor.getProblems().keySet(), empty());
    }

    @Test
    public void shouldDoNothingOnNullResolve() throws Exception {
        monitor.registerProblem(REPO, new IOException());
        monitor.resolveProblem(null);

        assertThat("should not change anything", monitor.isProblemWith(REPO), is(true));
    }

    @Test
    public void shouldBeDeactivatedByDefault() throws Exception {
        assertThat("should be deactivated", monitor.isActivated(), is(false));
    }

    @Test
    public void shouldBeActivatedOnProblems() throws Exception {
        monitor.registerProblem(REPO, new IOException());
        assertThat("active on problems", monitor.isActivated(), is(true));
    }

    @Test
    public void shouldResolveOnIgnoring() throws Exception {
        monitor.registerProblem(REPO, new IOException());
        monitor.doIgnore(REPO);

        assertThat("should be no problem", monitor.isProblemWith(REPO), is(false));
    }

    @Test
    public void shouldNotRegisterNewOnIgnoring() throws Exception {
        monitor.doIgnore(REPO);
        monitor.registerProblem(REPO, new IOException());

        assertThat("should be no problem", monitor.isProblemWith(REPO), is(false));
    }

    @Test
    public void shouldRemoveFromIgnoredOnDisignore() throws Exception {
        monitor.doIgnore(REPO);
        monitor.doDisignore(REPO);

        assertThat("should be no problem", monitor.getIgnored(), hasSize(0));
    }

    @Test
    public void shouldNotAddRepoTwiceToIgnore() throws Exception {
        monitor.doIgnore(REPO);
        monitor.doIgnore(REPO);

        assertThat("twice ignored", monitor.getIgnored(), hasSize(1));
    }

    @Test
    @LocalData
    public void shouldLoadIgnoredList() throws Exception {
        assertThat("loaded", monitor.getIgnored(), hasItem(equalTo(REPO)));
    }

    @Test
    public void shouldReportAboutHookProblemOnRegister() throws IOException {
        FreeStyleProject job = jRule.createFreeStyleProject();
        job.addTrigger(new GitHubPushTrigger());