/*
 * The MIT License
 *
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly, InfraDNA, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package hudson.scm;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import hudson.AbortException;
import hudson.DescriptorExtensionList;
import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.FilePath;
import hudson.Functions;
import hudson.Launcher;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Api;
import hudson.model.BuildListener;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.Job;
import hudson.model.Node;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.TopLevelItemDescriptor;
import hudson.model.WorkspaceCleanupThread;
import hudson.security.Permission;
import hudson.security.PermissionGroup;
import hudson.security.PermissionScope;
import hudson.tasks.Builder;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import jenkins.util.SystemProperties;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;

/**
 * Captures the configuration information in it.
 *
 * <p>
 * To register a custom {@link SCM} implementation from a plugin,
 * put {@link Extension} on your {@link SCMDescriptor}.
 *
 * <p>
 * Use the "project-changes" view to render change list to be displayed
 * at the project level. The default implementation simply aggregates
 * change lists from builds, but your SCM can provide different views.
 * The view gets the "builds" variable which is a list of builds that are
 * selected for the display.
 *
 * <p>
 * If you are interested in writing a subclass in a plugin,
 * also take a look at <a href="https://www.jenkins.io/doc/developer/plugin-development/writing-an-scm-plugin/">
 * "Writing an SCM plugin"</a> wiki article.
 *
 * @author Kohsuke Kawaguchi
 */
@ExportedBean
public abstract class SCM implements Describable<SCM>, ExtensionPoint {

    private static final Logger LOGGER = Logger.getLogger(SCM.class.getName());

    /** JENKINS-35098: discouraged */
    @SuppressWarnings("FieldMayBeFinal")
    private static boolean useAutoBrowserHolder = SystemProperties.getBoolean(SCM.class.getName() + ".useAutoBrowserHolder");
    /**
     * Stores {@link AutoBrowserHolder}. Lazily created.
     * @deprecated Unused by default.
     */
    @Deprecated
    private transient AutoBrowserHolder autoBrowserHolder;

    /**
     * Expose {@link SCM} to the remote API.
     */
    public Api getApi() {
        return new Api(this);
    }

    /**
     * Returns the {@link RepositoryBrowser} for files
     * controlled by this {@link SCM}.
     *
     * @return
     *      null to indicate that there's no explicitly configured browser
     *      for this SCM instance.
     *
     * @see #getEffectiveBrowser()
     */
    public @CheckForNull RepositoryBrowser<?> getBrowser() {
        return null;
    }

    /**
     * Type of this SCM.
     *
     * Exposed so that the client of the remote API can tell what SCM this is.
     */
    @Exported
    public String getType() {
        return getClass().getName();
    }

    /**
     * Returns the applicable {@link RepositoryBrowser} for files
     * controlled by this {@link SCM}.
     * @see #guessBrowser
     */
    @SuppressWarnings("deprecation")
    @Exported(name = "browser")
    public final @CheckForNull RepositoryBrowser<?> getEffectiveBrowser() {
        RepositoryBrowser<?> b = getBrowser();
        if (b != null)
            return b;
        if (useAutoBrowserHolder) {
            if (autoBrowserHolder == null) {
                autoBrowserHolder = new AutoBrowserHolder(this);
            }
            return autoBrowserHolder.get();
        } else {
            try {
                return guessBrowser();
            } catch (RuntimeException x) {
                LOGGER.log(Level.WARNING, null, x);
                return null;
            }
        }
    }

    /**
     * Returns true if this SCM supports
     * {@link #poll(AbstractProject, Launcher, FilePath, TaskListener, SCMRevisionState) poling}.
     *
     * @since 1.105
     */
    public boolean supportsPolling() {
        return true;
    }

    /**
     * Returns true if this SCM requires a checked out workspace for doing polling.
     *
     * <p>
     * This flag affects the behavior of Hudson when a job lost its workspace
     * (typically due to a agent outage.) If this method returns true and
     * polling is configured, then that would usually trigger a new build.
     *
     * <p>
     * This flag also affects the mutual exclusion control between builds and polling.
     * If this methods returns false, polling will continue asynchronously even
     * when a build is in progress, but otherwise the polling activity is blocked
     * if a build is currently using a workspace.
     *
     * <p>
     * The default implementation returns true.
     *
     * <p>
     * See issue JENKINS-1348 for more discussion of this feature.
     *
     * @since 1.196
     */
    public boolean requiresWorkspaceForPolling() {
        return true;
    }

    /**
     * Called before a workspace is deleted on the given node, to provide SCM an opportunity to perform clean up.
     *
     * <p>
     * Hudson periodically scans through all the agents and removes old workspaces that are deemed unnecessary.
     * This behavior is implemented in {@link WorkspaceCleanupThread}, and it is necessary to control the
     * disk consumption on agents. If we don't do this, in a long run, all the agents will have workspaces
     * for all the projects, which will be prohibitive in big Hudson.
     *
     * <p>
     * However, some SCM implementations require that the server be made aware of deletion of the local workspace,
     * and this method provides an opportunity for SCMs to perform such a clean-up act.
     *
     * <p>
     * This call back is invoked after Hudson determines that a workspace is unnecessary, but before the actual
     * recursive directory deletion happens.
     *
     * <p>
     * Note that this method does not guarantee that such a clean up will happen. For example, agents can be
     * taken offline by being physically removed from the network, and in such a case there's no opportunity
     * to perform this clean up.
     *
     * <p>
     * This method is also invoked when the project is deleted.
     *
     * @param project
     *      The project that owns this {@link SCM}. This is always the same object for a particular instance
     *      of {@link SCM}. Just passed in here so that {@link SCM} itself doesn't have to remember the value.
     * @param workspace
     *      The workspace which is about to be deleted. This can be a remote file path.
     * @param node
     *      The node that hosts the workspace. SCM can use this information to determine the course of action.
     *
     * @return
     *      true if {@link SCM} is OK to let Hudson proceed with deleting the workspace.
     *      False to veto the workspace deletion.
     *
     * @since 1.568
     */
    public boolean processWorkspaceBeforeDeletion(@NonNull Job<?, ?> project, @NonNull FilePath workspace, @NonNull Node node) throws IOException, InterruptedException {
        if (project instanceof AbstractProject) {
            return processWorkspaceBeforeDeletion((AbstractProject) project, workspace, node);
        } else {
            return true;
        }
    }

    @Deprecated
    public boolean processWorkspaceBeforeDeletion(AbstractProject<?, ?> project, FilePath workspace, Node node) throws IOException, InterruptedException {
        if (Util.isOverridden(SCM.class, getClass(), "processWorkspaceBeforeDeletion", Job.class, FilePath.class, Node.class)) {
            return processWorkspaceBeforeDeletion((Job) project, workspace, node);
        } else {
            return true;
        }
    }

    /**
     * Checks if there has been any changes to this module in the repository.
     *
     * TODO: we need to figure out a better way to communicate an error back,
     * so that we won't keep retrying the same node (for example an agent might be down.)
     *
     * <p>
     * If the SCM doesn't implement polling, have the {@link #supportsPolling()} method
     * return false.
     *
     * @param project
     *      The project to check for updates
     * @param launcher
     *      Abstraction of the machine where the polling will take place. If SCM declares
     *      that {@linkplain #requiresWorkspaceForPolling() the polling doesn't require a workspace}, this parameter is null.
     * @param workspace
     *      The workspace directory that contains baseline files. If SCM declares
     *      that {@linkplain #requiresWorkspaceForPolling() the polling doesn't require a workspace}, this parameter is null.
     * @param listener
     *      Logs during the polling should be sent here.
     *
     * @return true
     *      if the change is detected.
     *
     * @throws InterruptedException
     *      interruption is usually caused by the user aborting the computation.
     *      this exception should be simply propagated all the way up.
     *
     * @see #supportsPolling()
     *
     * @deprecated as of 1.345
     *      Override {@link #calcRevisionsFromBuild(AbstractBuild, Launcher, TaskListener)} and
     *      {@link #compareRemoteRevisionWith(AbstractProject, Launcher, FilePath, TaskListener, SCMRevisionState)} for implementation.
     *
     *      The implementation is now separated in two pieces, one that computes the revision of the current workspace,
     *      and the other that computes the revision of the remote repository.
     *
     *      Call {@link #poll(AbstractProject, Launcher, FilePath, TaskListener, SCMRevisionState)} for use instead.
     */
    @Deprecated
    public boolean pollChanges(AbstractProject<?, ?> project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException, InterruptedException {
        // up until 1.336, this method was abstract, so everyone should have overridden this method
        // without calling super.pollChanges. So the compatibility implementation is purely for
        // new implementations that doesn't override this method.
        throw new AbstractMethodError("you must override compareRemoteRevisionWith");
    }

    /**
     * Calculates the {@link SCMRevisionState} that represents the state of the workspace of the given build.
     *
     * <p>
     * The returned object is then fed into the
     * {@link #compareRemoteRevisionWith(AbstractProject, Launcher, FilePath, TaskListener, SCMRevisionState)} method
     * as the baseline {@link SCMRevisionState} to determine if the build is necessary.
     *
     * <p>
     * This method is called after source code is checked out for the given build (that is, after
     * {@link SCM#checkout(Run, Launcher, FilePath, TaskListener, File, SCMRevisionState)} has finished successfully.)
     *
     * <p>
     * The obtained object is added to the build as an {@link Action} for later retrieval. As an optimization,
     * {@link SCM} implementation can choose to compute {@link SCMRevisionState} and add it as an action
     * during check out, in which case this method will not called.
     *
     * @param build
     *      The calculated {@link SCMRevisionState} is for the files checked out in this build.
     *      If {@link #requiresWorkspaceForPolling()} returns true, Hudson makes sure that the workspace of this
     *      build is available and accessible by the callee.
     * @param workspace the location of the checkout; normally not null, since this will normally be called immediately after checkout,
     *                  though could be null if data is being loaded from a very old version of Jenkins and the SCM declares that it does not require a workspace for polling
     * @param launcher
     *      Abstraction of the machine where the polling will take place. Nullness matches that of {@code workspace}.
     * @param listener
     *      Logs during the polling should be sent here.
     *
     * @throws InterruptedException
     *      interruption is usually caused by the user aborting the computation.
     *      this exception should be simply propagated all the way up.
     * @since 1.568
     */
    public @CheckForNull SCMRevisionState calcRevisionsFromBuild(@NonNull Run<?, ?> build, @Nullable FilePath workspace, @Nullable Launcher launcher, @NonNull TaskListener listener) throws IOException, InterruptedException {
        if (build instanceof AbstractBuild && Util.isOverridden(SCM.class, getClass(), "calcRevisionsFromBuild", AbstractBuild.class, Launcher.class, TaskListener.class)) {
            return calcRevisionsFromBuild((AbstractBuild) build, launcher, listener);
        } else {
            throw new AbstractMethodError("you must override the new calcRevisionsFromBuild overload");
        }
    }

    @Deprecated
    public SCMRevisionState calcRevisionsFromBuild(AbstractBuild<?, ?> build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
        return calcRevisionsFromBuild(build, launcher != null ? build.getWorkspace() : null, launcher, listener);
    }

    @Deprecated
    public SCMRevisionState _calcRevisionsFromBuild(AbstractBuild<?, ?> build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
        return calcRevisionsFromBuild(build, launcher, listener);
    }

    /**
     * Compares the current state of the remote repository against the given baseline {@link SCMRevisionState}.
     *
     * <p>
     * Conceptually, the act of polling is to take two states of the repository and to compare them to see
     * if there's any difference. In practice, however, comparing two arbitrary repository states is an expensive
     * operation, so in this abstraction, we chose to mix (1) the act of building up a repository state and
     * (2) the act of comparing it with the earlier state, so that SCM implementations can implement this
     * more easily.
     *
     * <p>
     * Multiple invocations of this method may happen over time to make sure that the remote repository
     * is "quiet" before Hudson schedules a new build.
     *
     * @param project
     *      The project to check for updates
     * @param launcher
     *      Abstraction of the machine where the polling will take place. If SCM declares
     *      that {@linkplain #requiresWorkspaceForPolling() the polling doesn't require a workspace}, this parameter is null.
     * @param workspace
     *      The workspace directory that contains baseline files. If SCM declares
     *      that {@linkplain #requiresWorkspaceForPolling() the polling doesn't require a workspace}, this parameter is null.
     * @param listener
     *      Logs during the polling should be sent here.
     * @param baseline
     *      The baseline of the comparison. This object is the return value from earlier
     *      {@link #compareRemoteRevisionWith(AbstractProject, Launcher, FilePath, TaskListener, SCMRevisionState)} or
     *      {@link #calcRevisionsFromBuild(AbstractBuild, Launcher, TaskListener)}.
     *
     * @return
     *      This method returns multiple values that are bundled together into the {@link PollingResult} value type.
     *      {@link PollingResult#baseline} should be the value of the baseline parameter, {@link PollingResult#remote}
     *      is the current state of the remote repository (this object only needs to be understandable to the future
     *      invocations of this method),
     *      and {@link PollingResult#change} that indicates the degree of changes found during the comparison.
     *
     * @throws InterruptedException
     *      interruption is usually caused by the user aborting the computation.
     *      this exception should be simply propagated all the way up.
     * @since 1.568
     */
    public PollingResult compareRemoteRevisionWith(
            @NonNull Job<?, ?> project,
            @Nullable Launcher launcher,
            @Nullable FilePath workspace,
            @NonNull TaskListener listener,
            @NonNull SCMRevisionState baseline)
            throws IOException, InterruptedException {
        if (project instanceof AbstractProject
                && Util.isOverridden(
                        SCM.class,
                        getClass(),
                        "compareRemoteRevisionWith",
                        AbstractProject.class,
                        Launcher.class,
                        FilePath.class,
                        TaskListener.class,
                        SCMRevisionState.class)) {
            return compareRemoteRevisionWith((AbstractProject) project, launcher, workspace, listener, baseline);
        } else {
            throw new AbstractMethodError("you must override the new overload of compareRemoteRevisionWith");
        }
    }

    @Deprecated
    protected PollingResult compareRemoteRevisionWith(AbstractProject<?, ?> project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException {
        return compareRemoteRevisionWith((Job) project, launcher, workspace, listener, baseline);
    }

    /**
     * Convenience method for the caller to handle the backward compatibility between pre 1.345 SCMs.
     */
    public final PollingResult poll(AbstractProject<?, ?> project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException {
        if (is1_346OrLater()) {
            // This is to work around JENKINS-5827 in a general way.
            // don't let the SCM.compareRemoteRevisionWith(...) see SCMRevisionState that it didn't produce.
            SCMRevisionState baseline2;
            if (baseline != SCMRevisionState.NONE) {
                baseline2 = baseline;
            } else {
                baseline2 = calcRevisionsFromBuild(project.getLastBuild(), launcher, listener);
            }

            return compareRemoteRevisionWith(project, launcher, workspace, listener, baseline2);
        } else {
            return pollChanges(project, launcher, workspace, listener) ? PollingResult.SIGNIFICANT : PollingResult.NO_CHANGES;
        }
    }

    private boolean is1_346OrLater() {
        for (Class<?> c = getClass(); c != SCM.class; c = c.getSuperclass()) {
            try {
                c.getDeclaredMethod("compareRemoteRevisionWith", AbstractProject.class, Launcher.class, FilePath.class, TaskListener.class, SCMRevisionState.class);
                return true;
            } catch (NoSuchMethodException e) {
                try {
                    c.getDeclaredMethod("compareRemoteRevisionWith", Job.class, Launcher.class, FilePath.class, TaskListener.class, SCMRevisionState.class);
                    return true;
                } catch (NoSuchMethodException e2) { }
            }
        }
        return false;
    }

    /**
     * Should create a key by which this SCM configuration might be distinguished from others in the same project.
     * Should be invariable across builds but otherwise as distinctive as possible.
     * <p>Could include information such as the relative paths used in {@link #getModuleRoots(FilePath, AbstractBuild)},
     * and/or configured repository URLs and branch names, and/or labels set for this purpose by the user.
     * <p>The result may be used for various purposes, but it may be long and/or include URL-unsafe characters,
     * so to use in a URL path component you may need to first wrap it in {@link Util#getDigestOf(String)} or otherwise encode it.
     * @return by default, just {@link #getType}
     * @since 1.568
     */
    public @NonNull String getKey() {
        return getType();
    }

    /**
     * Obtains a fresh workspace of the module(s) into the specified directory
     * of the specified machine.
     *
     * <p>
     * The "update" operation can be performed instead of a fresh checkout if
     * feasible.
     *
     * <p>
     * This operation should also capture the information necessary to tag the workspace later.
     *
     * @param launcher
     *      Abstracts away the machine that the files will be checked out.
     * @param workspace
     *      a directory to check out the source code. May contain left-over
     *      from the previous build.
     * @param changelogFile
     *      Upon a successful return, this file should capture the changelog.
     *      When there's no change, this file should contain an empty entry.
     *      See {@link #createEmptyChangeLog(File, TaskListener, String)}.
     *      May be null, in which case no changelog was requested.
     * @param  baseline version from the previous build to use for changelog creation, if requested and available
     * @throws InterruptedException
     *      interruption is usually caused by the user aborting the build.
     *      this exception will cause the build to be aborted.
     * @throws AbortException in case of a routine failure
     * @since 1.568
     */
    public void checkout(
            @NonNull Run<?, ?> build,
            @NonNull Launcher launcher,
            @NonNull FilePath workspace,
            @NonNull TaskListener listener,
            @CheckForNull File changelogFile,
            @CheckForNull SCMRevisionState baseline)
            throws IOException, InterruptedException {
        if (build instanceof AbstractBuild
                && listener instanceof BuildListener
                && Util.isOverridden(
                        SCM.class,
                        getClass(),
                        "checkout",
                        AbstractBuild.class,
                        Launcher.class,
                        FilePath.class,
                        BuildListener.class,
                        File.class)) {
            if (changelogFile == null) {
                changelogFile = File.createTempFile("changelog", ".xml");
                try {
                    if (!checkout((AbstractBuild) build, launcher, workspace, (BuildListener) listener, changelogFile)) {
                        throw new AbortException();
                    }
                } finally {
                    Util.deleteFile(changelogFile);
                }
            } else {
                if (!checkout((AbstractBuild) build, launcher, workspace, (BuildListener) listener, changelogFile)) {
                    throw new AbortException();
                }
            }
        } else {
            throw new AbstractMethodError("you must override the new overload of checkout");
        }
    }

    @Deprecated
    public boolean checkout(AbstractBuild<?, ?> build, Launcher launcher, FilePath workspace, BuildListener listener, @NonNull File changelogFile) throws IOException, InterruptedException {
        AbstractBuild<?, ?> prev = build.getPreviousBuild();
        checkout((Run) build, launcher, workspace, listener, changelogFile, prev != null ? prev.getAction(SCMRevisionState.class) : null);
        return true;
    }

    /**
     * Get a chance to do operations after the workspace i checked out and the changelog is written.
     * @since 1.568
     */
    public void postCheckout(@NonNull Run<?, ?> build, @NonNull Launcher launcher, @NonNull FilePath workspace, @NonNull TaskListener listener) throws IOException, InterruptedException {
        if (build instanceof AbstractBuild && listener instanceof BuildListener) {
            postCheckout((AbstractBuild) build, launcher, workspace, (BuildListener) listener);
        }
    }

    @Deprecated
    public void postCheckout(AbstractBuild<?, ?> build, Launcher launcher, FilePath workspace, BuildListener listener) throws IOException, InterruptedException {
        if (Util.isOverridden(SCM.class, getClass(), "postCheckout", Run.class, Launcher.class, FilePath.class, TaskListener.class)) {
            postCheckout((Run) build, launcher, workspace, listener);
        }
        /* Default implementation is noop */
    }

    /**
     * Adds environmental variables for the builds to the given map.
     *
     * <p>
     * This can be used to propagate information from SCM to builds
     * (for example, SVN revision number.)
     *
     * <p>
     * This method is invoked whenever someone does {@link AbstractBuild#getEnvironment(TaskListener)}, via
     * {@link #buildEnvVars(AbstractBuild, Map)}, which can be before/after your checkout method is invoked. So if you
     * are going to provide information about check out (like SVN revision number that was checked out), be prepared
     * for the possibility that the check out hasn't happened yet.
     *
     * @since 2.60
     */
    public void buildEnvironment(@NonNull Run<?, ?> build, @NonNull Map<String, String> env) {
        if (build instanceof AbstractBuild) {
            buildEnvVars((AbstractBuild) build, env);
        }
    }

    /**
     * @deprecated in favor of {@link #buildEnvironment(Run, Map)}.
     */
    @Deprecated
    public void buildEnvVars(AbstractBuild<?, ?> build, Map<String, String> env) {
        if (Util.isOverridden(SCM.class, getClass(), "buildEnvironment", Run.class, Map.class)) {
            buildEnvironment(build, env);
        }
        // default implementation is noop.
    }

    /**
     * Gets the top directory of the checked out module.
     *
     * <p>
     * Often SCMs have to create a directory inside a workspace, which
     * creates directory layout like this:
     *
     * <pre>{@code
     * workspace  <- workspace root
     *  +- xyz    <- directory checked out by SCM
     *      +- CVS
     *      +- build.xml  <- user file
     * }</pre>
     *
     * <p>
     * Many builders, like Ant or Maven, works off the specific user file
     * at the top of the checked out module (in the above case, that would
     * be {@code xyz/build.xml}), yet the builder doesn't know the "xyz"
     * part; that comes from SCM.
     *
     * <p>
     * Collaboration between {@link Builder} and {@link SCM} allows
     * Hudson to find build.xml without asking the user to enter "xyz" again.
     *
     * <p>
     * This method is for this purpose. It takes the workspace
     * root as a parameter, and expected to return the directory
     * that was checked out from SCM.
     *
     * <p>
     * If this SCM is configured to create a directory, try to
     * return that directory so that builders can work seamlessly.
     *
     * <p>
     * If SCM doesn't need to create any directory inside workspace,
     * or in any other tricky cases, it should revert to the default
     * implementation, which is to just return the parameter.
     *
     * @param workspace
     *      The workspace root directory.
     * @param build
     *      The build for which the module root is desired.
     *      This parameter is null when existing legacy code calls deprecated {@link #getModuleRoot(FilePath)}.
     *      Handle this situation gracefully if your can, but otherwise you can just fail with an exception, too.
     *
     * @since 1.382
     */
    // TODO perhaps deprecate all replace with a single getModuleRoots(FilePath, Run)
    public FilePath getModuleRoot(FilePath workspace, AbstractBuild build) {
        // For backwards compatibility, call the one argument version of the method.
        return getModuleRoot(workspace);
    }

    /**
     * @deprecated since 1.382
     *      Use/override {@link #getModuleRoot(FilePath, AbstractBuild)} instead.
     */
    @Deprecated
    public FilePath getModuleRoot(FilePath workspace) {
        if (Util.isOverridden(SCM.class, getClass(), "getModuleRoot", FilePath.class, AbstractBuild.class))
            // if the subtype already implements newer getModuleRoot(FilePath,AbstractBuild), call that.
            return getModuleRoot(workspace, null);

        return workspace;
    }

    /**
     * Gets the top directories of all the checked out modules.
     *
     * <p>
     * Some SCMs support checking out multiple modules inside a workspace, which
     * creates directory layout like this:
     *
     * <pre>{@code
     * workspace  <- workspace root
     *  +- xyz    <- directory checked out by SCM
     *      +- .svn
     *      +- build.xml  <- user file
     *  +- abc    <- second module from different SCM root
     *      +- .svn
     *      +- build.xml  <- user file
     * }</pre>
     *
     * This method takes the workspace root as a parameter, and is expected to return
     * all the module roots that were checked out from SCM.
     *
     * <p>
     * For normal SCMs, the array will be of length {@code 1} and it's contents
     * will be identical to calling {@link #getModuleRoot(FilePath, AbstractBuild)}.
     *
     * @param workspace The workspace root directory
     * @param build
     *      The build for which the module roots are desired.
     *      This parameter is null when existing legacy code calls deprecated {@link #getModuleRoot(FilePath)}.
     *      Handle this situation gracefully if your can, but otherwise you can just fail with an exception, too.
     *
     * @return An array of all module roots.
     * @since 1.382
     */
    public FilePath[] getModuleRoots(FilePath workspace, AbstractBuild build) {
        if (Util.isOverridden(SCM.class, getClass(), "getModuleRoots", FilePath.class))
            // if the subtype derives legacy getModuleRoots(FilePath), delegate to it
            return getModuleRoots(workspace);

        // otherwise the default implementation
        return new FilePath[]{getModuleRoot(workspace, build)};
    }

    /**
     * @deprecated as of 1.382.
     *      Use/derive from {@link #getModuleRoots(FilePath, AbstractBuild)} instead.
     */
    @Deprecated
    public FilePath[] getModuleRoots(FilePath workspace) {
        if (Util.isOverridden(SCM.class, getClass(), "getModuleRoots", FilePath.class, AbstractBuild.class))
            // if the subtype already derives newer getModuleRoots(FilePath,AbstractBuild), delegate to it
            return getModuleRoots(workspace, null);

        // otherwise the default implementation
        return new FilePath[] { getModuleRoot(workspace), };
    }

    /**
     * The returned object will be used to parse {@code changelog.xml}.
     */
    public abstract ChangeLogParser createChangeLogParser();

    @Override
    public SCMDescriptor<?> getDescriptor() {
        return (SCMDescriptor) Jenkins.get().getDescriptorOrDie(getClass());
    }

//
// convenience methods
//

    @Deprecated
    protected final boolean createEmptyChangeLog(File changelogFile, BuildListener listener, String rootTag) {
        try {
            createEmptyChangeLog(changelogFile, (TaskListener) listener, rootTag);
            return true;
        } catch (IOException e) {
            Functions.printStackTrace(e, listener.error(e.getMessage()));
            return false;
        }
    }

    /**
     * @since 1.568
     */
    protected final void createEmptyChangeLog(@NonNull File changelogFile, @NonNull TaskListener listener, @NonNull String rootTag) throws IOException {
        try (Writer w = Files.newBufferedWriter(Util.fileToPath(changelogFile), Charset.defaultCharset())) {
            w.write("<" + rootTag + "/>");
        }
    }

    protected final String nullify(String s) {
        if (s == null)     return null;
        if (s.trim().isEmpty())    return null;
        return s;
    }

    public static final PermissionGroup PERMISSIONS = new PermissionGroup(SCM.class, Messages._SCM_Permissions_Title());
    /**
     * Permission to create new tags.
     * @since 1.171
     */
    public static final Permission TAG = new Permission(PERMISSIONS, "Tag", Messages._SCM_TagPermission_Description(), Permission.CREATE, PermissionScope.ITEM);

    /**
     * Returns all the registered {@link SCMDescriptor}s.
     */
    public static DescriptorExtensionList<SCM, SCMDescriptor<?>> all() {
        return Jenkins.get().getDescriptorList(SCM.class);
    }

    /**
     * Determines which kinds of SCMs are applicable to a given project.
     * @param project a project on which we might be configuring SCM, or null if unknown
     * @return all descriptors which {@link SCMDescriptor#isApplicable(Job)} to it, also filtered by {@link TopLevelItemDescriptor#isApplicable};
     *         or simply {@link #all} if there is no project
     * @since 1.568
     */
    public static List<SCMDescriptor<?>> _for(@CheckForNull final Job project) {
        if (project == null)   return all();

        final Descriptor pd = Jenkins.get().getDescriptor((Class) project.getClass());
        List<SCMDescriptor<?>> r = new ArrayList<>();
        for (SCMDescriptor<?> scmDescriptor : all()) {
            if (!scmDescriptor.isApplicable(project))    continue;

            if (pd instanceof TopLevelItemDescriptor) {
                TopLevelItemDescriptor apd = (TopLevelItemDescriptor) pd;
                if (!apd.isApplicable(scmDescriptor))    continue;
            }

            r.add(scmDescriptor);
        }

        return r;
    }

    @Deprecated
    public static List<SCMDescriptor<?>> _for(final AbstractProject project) {
        return _for((Job) project);
    }

    /**
     * Try to guess how a repository browser should be configured, based on URLs and the like.
     * Used when {@link #getBrowser} has not been explicitly configured.
     * @return a reasonable default value for {@link #getEffectiveBrowser}, or null
     * @since 1.568
     */
    public @CheckForNull RepositoryBrowser<?> guessBrowser() {
        return null;
    }

}
