public abstract class GitHubCause<T extends GitHubCause<T>> extends Cause {
    private static final Logger LOG = LoggerFactory.getLogger(GitHubCause.class);

    private boolean skip;
    private String reason;

    /**
     * Doesn't exist for deleted branch.
     */
    @CheckForNull
    private URL htmlUrl;

    @CheckForNull
    private String title;

    private String gitUrl;
    private String sshUrl;

    private String pollingLog;

    private transient Object remoteData;

    public GitHubCause withLocalRepo(@NonNull GitHubRepository localRepo) {
        withGitUrl(localRepo.getGitUrl());
        withSshUrl(localRepo.getSshUrl());
        withHtmlUrl(localRepo.getGithubUrl());
        return this;
    }

    /**
     * When set and event got positive condition says to skip job triggering.
     */
    public boolean isSkip() {
        return skip;
    }

    public GitHubCause<T> withSkip(boolean skip) {
        this.skip = skip;
        return this;
    }

    /**
     * For printing branch url on left builds panel (build description).
     */
    @CheckForNull
    public URL getHtmlUrl() {
        return htmlUrl;
    }

    public GitHubCause<T> withHtmlUrl(URL htmlUrl) {
        this.htmlUrl = htmlUrl;
        return this;
    }

    public String getGitUrl() {
        return gitUrl;
    }

    public GitHubCause<T> withGitUrl(String gitUrl) {
        this.gitUrl = gitUrl;
        return this;
    }

    public String getSshUrl() {
        return sshUrl;
    }

    public GitHubCause<T> withSshUrl(String sshUrl) {
        this.sshUrl = sshUrl;
        return this;
    }

    public String getPollingLog() {
        return pollingLog;
    }

    public GitHubCause<T> withPollingLog(String pollingLog) {
        this.pollingLog = pollingLog;
        return this;
    }

    public void setPollingLog(String pollingLog) {
        this.pollingLog = pollingLog;
    }

    public void setPollingLogFile(File logFile) throws IOException {
        this.pollingLog = readFileToString(logFile);
    }

    public String getReason() {
        return reason;
    }

    public GitHubCause<T> withReason(String reason) {
        this.reason = reason;
        return this;
    }

    public Object getRemoteData() {
        return remoteData;
    }

    public GitHubCause<T> withRemoteData(Object remoteData) {
        this.remoteData = remoteData;
        return this;
    }

    /**
     * @return the title of the cause, never null.
     */
    @NonNull
    public String getTitle() {
        return nonNull(title) ? title : "";
    }

    public GitHubCause<T> withTitle(String title) {
        this.title = title;
        return this;
    }

    /**
     * @return at most the first 30 characters of the title, or
     */
    public String getAbbreviatedTitle() {
        return abbreviate(getTitle(), 30);
    }

    public abstract void fillParameters(List<ParameterValue> params);

    public abstract GitHubSCMHead<T> createSCMHead(String sourceId);

    @SuppressWarnings("unchecked")
    public GitHubSCMRevision createSCMRevision(String sourceId) {
        return createSCMHead(sourceId).createSCMRevision((T) this).setRemoteData(getRemoteData());
    }

    public static <T extends GitHubCause<T>> T skipTrigger(List<? extends T> causes) {
        if (isNull(causes)) {
            return null;
        }
        T cause = causes.stream()
                .filter(GitHubCause::isSkip)
                .findFirst()
                .orElse(null);

        return cause;
    }
} public class GitHubRepo implements Action {
    private static final Logger LOG = LoggerFactory.getLogger(GitHubRepo.class);

    private transient GitHubSCMSourcesLocalStorage owner;

    private GitHubBranchRepository branchRepository;
    private GitHubTagRepository tagRepository;
    private GitHubPRRepository prRepository;

    public GitHubRepo(GitHubSCMSourcesLocalStorage owner) {
        this.owner = owner;
    }

    /**
     * When remote side available.
     */
    public GitHubRepo(GHRepository repository) throws IOException {
        this(new GitHubBranchRepository(repository), new GitHubTagRepository(repository), new GitHubPRRepository(repository));
    }

    /**
     * For offline initialisation from what user specified.
     */
    public GitHubRepo(String repoFullName, URL url) {
        this(new GitHubBranchRepository(repoFullName, url), new GitHubTagRepository(repoFullName, url), new GitHubPRRepository(repoFullName, url));
    }

    public GitHubRepo(GitHubBranchRepository branchRepository, GitHubTagRepository tagRepository, GitHubPRRepository prRepository) {
        this.branchRepository = branchRepository;
        this.tagRepository = tagRepository;
        this.prRepository = prRepository;
    }

    public GitHubBranchRepository getBranchRepository() {
        return branchRepository;
    }

    public GitHubTagRepository getTagRepository() {
        return tagRepository;
    }

    public GitHubPRRepository getPrRepository() {
        return prRepository;
    }

    @CheckForNull
    @Override
    public String getIconFileName() {
        return null;
    }

    @CheckForNull
    @Override
    public String getDisplayName() {
        return "GitHub Local State";
    }

    @CheckForNull
    @Override
    public String getUrlName() {
        return null;
    }

    public void actualize(GHRepository remoteRepo) throws IOException {
        if (isNull(branchRepository)) {
            branchRepository = new GitHubBranchRepository(remoteRepo);
        } else {
            branchRepository.actualise(remoteRepo, TaskListener.NULL);
        }

        if (isNull(tagRepository)) {
            tagRepository = new GitHubTagRepository(remoteRepo);
        } else  {
            tagRepository.actualise(remoteRepo, TaskListener.NULL);
        }
        if (isNull(prRepository)) {
            prRepository = new GitHubPRRepository(remoteRepo);
        } else {
            prRepository.actualise(remoteRepo, TaskListener.NULL);
        }

        if (owner != null) {
            owner.save();
        }
    }

    public void sync(GHRepository remoteRepo) {

    }

    public void setOwner(GitHubSCMSourcesLocalStorage owner) {
        this.owner = owner;
    }


} public class GitHubSCMSourcesLocalStorage implements Saveable {
    private static final Logger LOG = LoggerFactory.getLogger(GitHubSCMSourcesLocalStorage.class);

    @NonNull
    protected transient PersistenceRoot project;

    private final String sourceId;
    private final GitHubRepo repoState;

    public GitHubSCMSourcesLocalStorage(@NonNull PersistenceRoot project, @NonNull String sourceId) {
        this.project = project;
        this.sourceId = sourceId;
        repoState = new GitHubRepo(this);
    }

    public GitHubRepo getLocalRepo() {
        return repoState;
    }

    public XmlFile getConfigFile() {
        return new XmlFile(new File(project.getRootDir(), "github-scm-" + normalizedSourceId() + ".xml"));
    }

    private String normalizedSourceId() {
        return DigestUtils.sha1Hex(sourceId);
    }

    public void saveQuietly() {
        try {
            save();
        } catch (IOException e) {
            LOG.error("Can't save repository state, because: '{}'", e.getMessage(), e);
        }
    }

    @Override
    public void save() throws IOException {
        synchronized (this) {
            if (BulkChange.contains(this)) {
                return;
            }

            getConfigFile().write(this);
        }
        SaveableListener.fireOnChange(this, getConfigFile());
    }


    public synchronized void load() throws IOException {
        if (getConfigFile().exists()) {
            getConfigFile().unmarshal(this);
        }
        repoState.setOwner(this);
    }

} public abstract class GitHubHandler extends AbstractDescribableImpl<GitHubHandler> {

    public abstract void handle(@NonNull GitHubSourceContext context) throws IOException;

    protected void processCauses(GitHubSourceContext context, Stream<? extends GitHubCause<?>> causeStream) throws IOException {
        GitHub github = context.getGitHub();
        TaskListener listener = context.getListener();

        listener.getLogger().println("GitHub rate limit before check: " + github.getRateLimit());

        // run the process
        forEachIo(causeStream, context::observe);

        listener.getLogger().println("GitHub rate limit after check: " + github.getRateLimit());
    }
} public class GitHubSourceContext {

    private final GitHubSCMSource source;
    private final SCMHeadObserver observer;
    private final SCMSourceCriteria criteria;
    private final SCMHeadEvent<?> scmHeadEvent;
    private final GitHubRepo localRepo;
    private final GHRepository remoteRepo;
    private final TaskListener listener;

    public GitHubSourceContext(@NonNull GitHubSCMSource source,
                               @NonNull SCMHeadObserver observer,
                               @NonNull SCMSourceCriteria criteria,
                               @Nullable SCMHeadEvent<?> scmHeadEvent,
                               @NonNull GitHubRepo localRepo,
                               @NonNull GHRepository remoteRepo,
                               @NonNull TaskListener listener) {
        this.source = source;
        this.observer = observer;
        this.criteria = criteria;
        this.scmHeadEvent = scmHeadEvent;
        this.localRepo = localRepo;
        this.remoteRepo = remoteRepo;
        this.listener = listener;
    }

    @NonNull
    public GitHubSCMSource getSource() {
        return source;
    }

    @NonNull
    public SCMHeadObserver getObserver() {
        return observer;
    }

    public SCMHeadEvent<?> getScmHeadEvent() {
        return scmHeadEvent;
    }

    @NonNull
    public GitHubRepo getLocalRepo() {
        return localRepo;
    }

    @NonNull
    public GHRepository getRemoteRepo() {
        return remoteRepo;
    }

    @NonNull
    public TaskListener getListener() {
        return listener;
    }

    public GitHub getGitHub() {
        return source.getRepoProvider().getGitHub(source);
    }

    public boolean checkCriteria(@NonNull GitHubCause<?> cause) throws IOException {
        if (nonNull(criteria)) {
            GitHubSCMRevision revision = cause.createSCMRevision(source.getId());
            listener.getLogger().println("");
            listener.getLogger().println("Checking " + revision.getHead().getPronoun());
            if (!criteria.isHead(source.newProbe(revision.getHead(), revision), listener)) {
                listener.getLogger().println("  Didn't meet criteria");
                return false;
            }
            listener.getLogger().println("  Met criteria");
        }
        return true;
    }

    public void observe(@NonNull GitHubCause<?> cause) {
        try {
            GitHubSCMRevision scmRevision = cause.createSCMRevision(source.getId());
            if (!cause.isSkip()) {
                forceScheduling(scmRevision);
            }
            getObserver().observe(scmRevision.getHead(), scmRevision);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace(listener.getLogger());
        }
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private void forceScheduling(GitHubSCMRevision scmRevision) throws IOException {
        SCMSourceOwner owner = source.getOwner();
        if (owner instanceof MultiBranchProject) {
            MultiBranchProject mb = (MultiBranchProject) owner;
            BranchProjectFactory pf = mb.getProjectFactory();

            SCMHead scmHead = scmRevision.getHead();
            Job j = mb.getItemByBranchName(scmHead.getName());
            if (j != null) {
                SCMRevision rev = pf.getRevision(j);
                // set current rev to dummy to force scheduling
                if (rev != null && rev.equals(scmRevision)) {
                    pf.setRevisionHash(j, new DummyRevision(scmHead));
                }
            }
        }
    }

    /**
     * Special revision to unconditionally force next build
     */
    public static class DummyRevision extends SCMRevision {
        private static final long serialVersionUID = 1L;

        public DummyRevision(SCMHead head) {
            super(head);
        }

        @Override
        public int hashCode() {
            return 0;
        }

        @Override
        public boolean equals(Object obj) {
            return false;
        }
    }
} public class GitHubBranchSCMHead extends GitHubSCMHead<GitHubBranchCause> {
    private static final long serialVersionUID = 1L;

    public GitHubBranchSCMHead(@NonNull String name, String sourceId) {
        super(name, sourceId);
    }

    @Override
    public String getPronoun() {
        return "Branch " + getName();
    }

    @Override
    public String fetchHeadSha(GHRepository remoteRepo) throws IOException {
        GHBranch branch = remoteRepo.getBranch(getName());
        if (branch == null) {
            throw new IOException("No branch " + getName() + " in " + remoteRepo.getFullName());
        }
        return branch.getSHA1();
    }

    @Override
    public String getHeadSha(GitHubBranchCause cause) {
        return cause.getCommitSha();
    }
} public class GitHubPRSCMHead extends GitHubSCMHead<GitHubPRCause> {
    private static final long serialVersionUID = 1L;

    private final int prNumber;
    private final String targetBranch;

    public GitHubPRSCMHead(@NonNull GitHubPRCause prCause, String sourceId) {
        this(prCause.getNumber(), prCause.getTargetBranch(), sourceId);
    }

    public GitHubPRSCMHead(@NonNull Integer prNumber, @NonNull String targetBranch, String sourceId) {
        super("pr-" + Integer.toString(prNumber), sourceId);
        this.prNumber = prNumber;
        this.targetBranch = targetBranch;
    }

    public int getPrNumber() {
        return prNumber;
    }

    public String getTargetBranch() {
        return targetBranch;
    }

    @Override
    public String getPronoun() {
        return "PR#" + prNumber;
    }

    @Override
    public String fetchHeadSha(GHRepository remoteRepo) throws IOException {
        GHPullRequest pullRequest = remoteRepo.getPullRequest(prNumber);
        if (pullRequest == null) {
            throw new IOException("No PR " + prNumber + " in " + remoteRepo.getFullName());
        }
        return pullRequest.getHead().getSha();
    }

    @Override
    public String getHeadSha(GitHubPRCause cause) {
        return cause.getHeadSha();
    }
} public abstract class GitHubSCMHead<T extends GitHubCause<T>> extends SCMHead {
    private static final long serialVersionUID = 1L;

    private final String sourceId;

    public GitHubSCMHead(@NonNull String name, String sourceId) {
        super(name);
        this.sourceId = sourceId;
    }

    public String getSourceId() {
        return sourceId;
    }

    public abstract String fetchHeadSha(GHRepository remoteRepo) throws IOException;

    public abstract String getHeadSha(T cause);

    public GitHubSCMRevision createSCMRevision(T cause) {
        return new GitHubSCMRevision(this, getHeadSha(cause), cause);
    }
} public class GitHubTagSCMHead extends GitHubSCMHead<GitHubTagCause> {
    private static final long serialVersionUID = 1L;

    public GitHubTagSCMHead(@NonNull String name, String sourceId) {
        super(name, sourceId);
    }

    @Override
    public String getPronoun() {
        return "Tag " + getName();
    }

    @Override
    public String fetchHeadSha(GHRepository remoteRepo) throws IOException {
        GHTag tag = GitHubTag.findRemoteTag(remoteRepo, getName());
        if (tag == null) {
            throw new IOException("No tag " + getName() + " in " + remoteRepo.getFullName());
        }
        return tag.getCommit().getSHA1();
    }

    public String getHeadSha(GitHubTagCause cause) {
        return cause.getCommitSha();
    }
} public abstract class GitHubRepoProvider2 extends AbstractDescribableImpl<GitHubRepoProvider2> implements ExtensionPoint {


    public abstract void registerHookFor(GitHubSCMSource source);

    public abstract boolean isManageHooks(GitHubSCMSource source);

    @NonNull
    public abstract GitHub getGitHub(GitHubSCMSource source);

    public abstract GHRepository getGHRepository(GitHubSCMSource source);

    public abstract static class GitHubRepoProviderDescriptor2
            extends Descriptor<GitHubRepoProvider2> {
        @NonNull
        public abstract String getDisplayName();

        public static DescriptorExtensionList allRepoProviders2() {
            return Jenkins.getInstance().getDescriptorList(GitHubRepoProvider2.class);
        }
    }
} public static final GitHubBranchSCMHeadCategory BRANCH = new GitHubBranchSCMHeadCategory(new NonLocalizable("Branches")); public static final GitHubPRSCMHeadCategory PR = new GitHubPRSCMHeadCategory("pr", new NonLocalizable("Pull Requests")); public static final GitHubTagSCMHeadCategory TAG = new GitHubTagSCMHeadCategory("tag", new NonLocalizable("Tags"));
import com.cloudbees.jenkins.GitHubRepositoryName;
import com.github.kostyasha.github.integration.generic.GitHubCause;
import com.github.kostyasha.github.integration.multibranch.action.GitHubBranchAction;
import com.github.kostyasha.github.integration.multibranch.action.GitHubLinkAction;
import com.github.kostyasha.github.integration.multibranch.action.GitHubPRAction;
import com.github.kostyasha.github.integration.multibranch.action.GitHubRepo;
import com.github.kostyasha.github.integration.multibranch.action.GitHubRepoAction;
import com.github.kostyasha.github.integration.multibranch.action.GitHubSCMSourcesLocalStorage;
import com.github.kostyasha.github.integration.multibranch.action.GitHubTagAction;
import com.github.kostyasha.github.integration.multibranch.fs.GitHubSCMProbe;
import com.github.kostyasha.github.integration.multibranch.fs.TreeCache;
import com.github.kostyasha.github.integration.multibranch.handler.GitHubHandler;
import com.github.kostyasha.github.integration.multibranch.handler.GitHubSourceContext;
import com.github.kostyasha.github.integration.multibranch.head.GitHubBranchSCMHead;
import com.github.kostyasha.github.integration.multibranch.head.GitHubPRSCMHead;
import com.github.kostyasha.github.integration.multibranch.head.GitHubSCMHead;
import com.github.kostyasha.github.integration.multibranch.head.GitHubTagSCMHead;
import com.github.kostyasha.github.integration.multibranch.repoprovider.GitHubRepoProvider2;
import com.github.kostyasha.github.integration.multibranch.revision.GitHubSCMRevision;
import com.github.kostyasha.github.integration.multibranch.scm.GitHubSCMFactory;
import com.github.kostyasha.github.integration.multibranch.scm.GitHubSCMFactory.GitHubSCMFactoryDescriptor;
import hudson.BulkChange;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.model.Action;
import hudson.model.CauseAction;
import hudson.model.ParameterValue;
import hudson.model.TaskListener;
import hudson.scm.SCM;
import jenkins.scm.api.SCMHead;
import jenkins.scm.api.SCMHeadCategory;
import jenkins.scm.api.SCMHeadEvent;
import jenkins.scm.api.SCMHeadObserver;
import jenkins.scm.api.SCMProbe;
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMSource;
import jenkins.scm.api.SCMSourceCriteria;
import jenkins.scm.api.SCMSourceDescriptor;
import jenkins.scm.api.SCMSourceEvent;
import jenkins.scm.api.SCMSourceOwner;
import jenkins.scm.api.metadata.ObjectMetadataAction;
import jenkins.scm.api.metadata.PrimaryInstanceMetadataAction;
import org.jenkinsci.Symbol;
import org.kohsuke.github.GHRepository;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static com.github.kostyasha.github.integration.multibranch.category.GitHubBranchSCMHeadCategory.BRANCH;
import static com.github.kostyasha.github.integration.multibranch.category.GitHubPRSCMHeadCategory.PR;
import static com.github.kostyasha.github.integration.multibranch.category.GitHubTagSCMHeadCategory.TAG;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
    }

    @NonNull
    @Override
    protected Set<SCMHead> retrieve(@CheckForNull SCMSourceCriteria criteria, @NonNull TaskListener listener)
            throws IOException, InterruptedException {
        return super.retrieve(criteria, listener);
    }

    @Override
    protected SCMRevision retrieve(@NonNull SCMHead head, @NonNull TaskListener listener)
            throws IOException, InterruptedException {
        return super.retrieve(head, listener);
    }

    @Override
    protected SCMRevision retrieve(@NonNull String thingName, @NonNull TaskListener listener)
            throws IOException, InterruptedException {
        return super.retrieve(thingName, listener);
    }

    @NonNull
    @Override
    protected Set<String> retrieveRevisions(@NonNull TaskListener listener) throws IOException, InterruptedException {
        return super.retrieveRevisions(listener);
    }

    @NonNull
    @Override
    protected List<Action> retrieveActions(@NonNull SCMRevision revision, @CheckForNull SCMHeadEvent event,
                                           @NonNull TaskListener listener) throws IOException, InterruptedException {
        GitHubSCMRevision gitHubSCMRevision = (GitHubSCMRevision) revision;
        GitHubCause<?> cause = gitHubSCMRevision.getCause();
        if (nonNull(cause)) {
            List<ParameterValue> params = new ArrayList<>();
            cause.fillParameters(params);
            return Arrays.asList(new CauseAction(cause), new GitHubParametersAction(params));
        }

        return Collections.emptyList();
    }

    @NonNull
    @Override
    protected List<Action> retrieveActions(@NonNull SCMHead head,
                                           @CheckForNull SCMHeadEvent event,
                                           @NonNull TaskListener listener)
            throws IOException, InterruptedException {
        List<Action> actions = new ArrayList<>();

        GHRepository remoteRepo = getRemoteRepo();

        boolean primary = false;
        GitHubLinkAction link = null;
        String desc = null;

        if (head instanceof GitHubBranchSCMHead) {
            // mark default branch item as primary
            primary = remoteRepo.getDefaultBranch().equals(head.getName());
            link = new GitHubBranchAction(remoteRepo, head.getName());
            desc = null;
        } else if (head instanceof GitHubTagSCMHead) {
            link = new GitHubTagAction(remoteRepo, head.getName());
            desc = null;
        } else if (head instanceof GitHubPRSCMHead) {
            GitHubPRSCMHead prHead = (GitHubPRSCMHead) head;
            link = new GitHubPRAction(remoteRepo, prHead.getPrNumber());
            desc = remoteRepo.getPullRequest(prHead.getPrNumber()).getTitle();
        }

        if (nonNull(link)) {
            actions.add(link);
        }

        actions.add(new ObjectMetadataAction(null, desc, isNull(link) ? null : link.getUrlName()));
        if (primary) {
            actions.add(new PrimaryInstanceMetadataAction());
        }

        return actions;
    }

    @NonNull
    @Override
    protected List<Action> retrieveActions(@CheckForNull SCMSourceEvent event, @NonNull TaskListener listener)
            throws IOException, InterruptedException {
        return Collections.singletonList(new GitHubRepoAction(getRemoteRepo()));
    }

    @NonNull
    @Override
    public SCMRevision getTrustedRevision(@NonNull SCMRevision revision, @NonNull TaskListener listener)
            throws IOException, InterruptedException {
        return super.getTrustedRevision(revision, listener);
    }

    @Override
    public boolean canProbe() {
        return true;
    }

    @NonNull
    @Override
    protected SCMProbe createProbe(@NonNull SCMHead head, @CheckForNull SCMRevision revision) throws IOException {
        return new GitHubSCMProbe(this, (GitHubSCMHead<?>) head, (GitHubSCMRevision) revision);
    }

    @Override
    protected boolean isCategoryEnabled(@NonNull SCMHeadCategory category) {
        return true;
    }

    @Symbol("gitHub")
    @Extension
    public static class DescriptorImpl extends SCMSourceDescriptor {

        @NonNull
        @Override
        protected SCMHeadCategory[] createCategories() {
            // array? bundled in descriptor?? seriously?
            return new SCMHeadCategory[]{BRANCH, PR, TAG};