package org.jenkinsci.plugins.gitclient;

import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Launcher.LocalLauncher;
import hudson.Proc;
import hudson.Util;
import hudson.console.HyperlinkNote;
import hudson.model.TaskListener;
import hudson.plugins.git.Branch;
import hudson.plugins.git.GitException;
import hudson.plugins.git.GitLockFailedException;
import hudson.plugins.git.GitObject;
import hudson.plugins.git.IGitAPI;
import hudson.plugins.git.IndexEntry;
import hudson.plugins.git.Revision;
import hudson.slaves.WorkspaceList;
import hudson.util.ArgumentListBuilder;
import hudson.util.Secret;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.AclEntry;
import java.nio.file.attribute.AclEntryPermission;
import java.nio.file.attribute.AclEntryType;
import java.nio.file.attribute.AclFileAttributeView;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.nio.file.attribute.UserPrincipal;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryBuilder;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteConfig;
import org.eclipse.jgit.transport.URIish;
import org.jenkinsci.plugins.gitclient.cgit.GitCommandsExecutor;
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted;
import org.kohsuke.stapler.framework.io.WriterOutputStream;

/**
 * Implementation class using command line CLI ran as external command.
 * <b>
 * For internal use only, don't use directly. See {@link org.jenkinsci.plugins.gitclient.Git}
 * </b>
 */
public class CliGitAPIImpl extends LegacyCompatibleGitAPIImpl {

    /**
     * Constant which can block use of setsid in git calls for ssh credentialed operations.
     *
     * <code>USE_SETSID=Boolean.valueOf(System.getProperty(CliGitAPIImpl.class.getName() + ".useSETSID", "false"))</code>.
     *
     * Allow ssh authenticated git calls on Unix variants to be preceded
     * by setsid so that the git command is run without being associated
     * with a terminal. Some docker runtime cases, and some automated test
     * cases have shown that some versions of command line git or ssh will
     * not allow automatic answers to private key passphrase prompts
     * unless there is no controlling terminal associated with the process.
     */
    public static final boolean USE_SETSID =
            Boolean.parseBoolean(System.getProperty(CliGitAPIImpl.class.getName() + ".useSETSID", "false"));

    /**
     * Set promptForAuthentication=true if you must allow command line git
     * versions 2.3 and later to prompt the user for authentication.
     *
     * Command line git prompting for authentication should be rare, since
     * Jenkins credentials should be managed through the credentials plugin.
     *
     * Command line git 2.3 and later read the environment variable
     * GIT_TERMINAL_PROMPT. If it has the value 0, then git will not prompt the
     * user for authentication, even if a terminal is available (as when running
     * a Jenkins agent from the Windows desktop, or when running it
     * interactively from the command line, or from a Docker image). If a
     * terminal is not available (most services on Windows and Linux), then
     * command line git will not prompt for authentication, whether or not
     * GIT_TERMINAL_PROMPT is set.
     *
     * GCM_INTERACTIVE=never is the environment variable which should
     * cause the git credential manager for windows to never prompt
     * for credentials.
     *
     * Credential prompting could happen on multiple platforms, but is
     * more common on Windows computers because many Windows agents
     * run from the desktop environment.  Agents running on the
     * desktop are much less common in the Unix environments.
     */
    private static final boolean PROMPT_FOR_AUTHENTICATION = Boolean.parseBoolean(
            System.getProperty(CliGitAPIImpl.class.getName() + ".promptForAuthentication", "false"));

    /**
     * CALL_SETSID decides if command line git can use the setsid program
     * during ssh based authentication to detach git from its controlling
     * terminal.
     *
     * If the controlling terminal remains attached, then ssh passphrase based
     * private keys cannot be decrypted during authentication (at least in some
     * ssh configurations).
     *
     * Package protected so that tests can override it.
     */
    static boolean CALL_SETSID;

    /**
     * Needed file permission for OpenSSH client that is made by Windows,
     * this will remove unwanted users and inherited permissions
     * which is required when the git client is using the SSH to clone
     *
     * The ssh client that the git client ships ignores file permission on Windows
     * Which the PowerShell team at Microsoft decided to fix in their port of OpenSSH
     */
    static final EnumSet<AclEntryPermission> ACL_ENTRY_PERMISSIONS = EnumSet.of(
            AclEntryPermission.READ_DATA,
            AclEntryPermission.WRITE_DATA,
            AclEntryPermission.APPEND_DATA,
            AclEntryPermission.READ_NAMED_ATTRS,
            AclEntryPermission.WRITE_NAMED_ATTRS,
            AclEntryPermission.EXECUTE,
            AclEntryPermission.READ_ATTRIBUTES,
            AclEntryPermission.WRITE_ATTRIBUTES,
            AclEntryPermission.DELETE,
            AclEntryPermission.READ_ACL,
            AclEntryPermission.SYNCHRONIZE);

    static {
        CALL_SETSID = setsidExists() && USE_SETSID;
    }

    /**
     * Constant which prevents use of 'force' in git fetch with CLI git versions 2.20 and later.
     *
     * <code>USE_FORCE_FETCH=Boolean.valueOf(System.getProperty(CliGitAPIImpl.class.getName() + ".forceFetch", "true"))</code>.
     *
     * Command line git 2.20 and later have changed fetch of remote
     * tags which already exist in the repository. Command line git
     * before 2.20 silently updates an existing tag if the remote tag
     * points to a different SHA1 than the local tag.  Command line
     * git 2.20 and later do not update an existing tag if the remote
     * tag points to a different SHA1 than the local tag unless the
     * 'force' option is passed to 'git fetch'.
     *
     * Use '-Dorg.jenkinsci.plugins.gitclient.CliGitAPIImpl.forceFetch=false'
     * to prevent 'force' in 'git fetch' with CLI git 2.20 and later.
     */
    private static final boolean USE_FORCE_FETCH =
            Boolean.parseBoolean(System.getProperty(CliGitAPIImpl.class.getName() + ".forceFetch", "true"));

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

    @Serial
    private static final long serialVersionUID = 1;

    static final String SPARSE_CHECKOUT_FILE_DIR = ".git/info";
    static final String SPARSE_CHECKOUT_FILE_PATH = ".git/info/sparse-checkout";
    static final String TIMEOUT_LOG_PREFIX = " # timeout=";
    private static final String INDEX_LOCK_FILE_PATH = ".git" + File.separator + "index.lock";
    transient Launcher launcher;
    TaskListener listener;
    String gitExe;
    EnvVars environment;
    private Map<String, StandardCredentials> credentials = new HashMap<>();
    private StandardCredentials defaultCredentials;
    private StandardCredentials lfsCredentials;
    private final String encoding;

    /* If we fail some helper tool (e.g. SELinux chcon) do not make noise
     * until actually git fails. Use a TreeMap to sort by keys (timestamp).
     */
    private Map<Instant, String> failureClues = new TreeMap<>();

    /* git config --get-regex applies the regex to match keys, and returns all matches (including substring matches).
     * Thus, a config call:
     *   git config -f .gitmodules --get-regexp "^submodule\.(.+)\.url"
     * will report two lines of output if the submodule URL includes ".url":
     *   submodule.modules/JENKINS-46504.url.path modules/JENKINS-46504.url
     *   submodule.modules/JENKINS-46504.url.url https://github.com/MarkEWaite/JENKINS-46054.url
     * The code originally used the same pattern for get-regexp and for output parsing.
     * By using the same pattern in both places, it incorrectly took some substrings
     * as the submodule remote name, instead of taking the longest match.
     * See SubmodulePatternStringTest for test cases.
     */
    private static final String SUBMODULE_REMOTE_PATTERN_CONFIG_KEY = "^submodule\\.(.+)\\.url";

    /* See comments for SUBMODULE_REMOTE_PATTERN_CONFIG_KEY to explain
     * why this regular expression string adds the trailing space
     * characters and the sequence of non-space characters as part of
     * its match.  The ending sequence of non-white-space characters
     * is the repository URL in the output of the 'git config' command.
     * Relies on repository URL not containing a whitespace character,
     * per RFC1738.
     */
    /* Package protected for testing */
    static final String SUBMODULE_REMOTE_PATTERN_STRING = SUBMODULE_REMOTE_PATTERN_CONFIG_KEY + "\\s+[^\\s]+$";

    private void warnIfWindowsTemporaryDirNameHasSpaces() {
        if (!isWindows()) {
            return;
        }
        String[] varsToCheck = {"TEMP", "TMP"};
        for (String envVar : varsToCheck) {
            String value = environment.get(envVar, "C:\\Temp");
            if (value.contains(" ")) {
                listener.getLogger()
                        .println("env " + envVar + "='" + value + "' contains an embedded space."
                                + " Some msysgit versions may fail credential related operations.");
            }
        }
    }

    // AABBCCDD where AA=major, BB=minor, CC=rev, DD=bugfix
    private long gitVersion = 0;

    private long computeVersionFromBits(int major, int minor, int rev, int bugfix) {
        return (major * 1000000L) + (minor * 10000L) + (rev * 100L) + bugfix;
    }

    private void getGitVersion() {
        if (gitVersion != 0) {
            return;
        }

        String version = "";
        try {
            version = launchCommand("--version").trim();
            listener.getLogger().println(" > git --version # '" + version + "'");
        } catch (Throwable e) {
        }

        computeGitVersion(version);
    }

    /* package */ void computeGitVersion(String version) {
        int gitMajorVersion = 0;
        int gitMinorVersion = 0;
        int gitRevVersion = 0;
        int gitBugfixVersion = 0;

        try {
            /*
             * msysgit adds one more term to the version number. So
             * instead of Major.Minor.Rev.Bugfix, it displays
             * something like Major.Minor.Rev.msysgit.BugFix. This
             * removes the inserted term from the version string
             * before parsing.
             * git 2.5.0 for windows adds a similar component with
             * the string "windows".  Remove it as well
             */

            String[] fields = version.split(" ")[2]
                    .replace("msysgit.", "")
                    .replace("windows.", "")
                    .split("\\.");

            gitMajorVersion = Integer.parseInt(fields[0]);
            gitMinorVersion = (fields.length > 1) ? Integer.parseInt(fields[1]) : 0;
            gitRevVersion = (fields.length > 2) ? Integer.parseInt(fields[2]) : 0;
            gitBugfixVersion = (fields.length > 3) ? Integer.parseInt(fields[3]) : 0;
        } catch (Throwable e) {
            /* Oh well */
        }

        gitVersion = computeVersionFromBits(gitMajorVersion, gitMinorVersion, gitRevVersion, gitBugfixVersion);
    }

    /* package */ boolean isAtLeastVersion(int major, int minor, int rev, int bugfix) {
        getGitVersion();
        long requestedVersion = computeVersionFromBits(major, minor, rev, bugfix);
        return gitVersion >= requestedVersion;
    }

    /**
     * Compare the current cli git version with the required version.
     * Finds if the current cli git version is at-least the required version.
     *
     * Returns True if the current cli git version is at least the required version.
     *
     * @param major required major version for command line git
     * @param minor required minor version for command line git
     * @param rev required revision for command line git
     * @param bugfix required patches for command line git
     * @return true if the command line git version is at least the required version
     **/
    public boolean isCliGitVerAtLeast(int major, int minor, int rev, int bugfix) {
        return isAtLeastVersion(major, minor, rev, bugfix);
    }

    /**
     * Constructor for CliGitAPIImpl.
     *
     * @param gitExe a {@link java.lang.String} object.
     * @param workspace a {@link java.io.File} object.
     * @param listener a {@link hudson.model.TaskListener} object.
     * @param environment a {@link hudson.EnvVars} object.
     */
    protected CliGitAPIImpl(String gitExe, File workspace, TaskListener listener, EnvVars environment) {
        super(workspace);
        this.listener = listener;
        this.gitExe = gitExe;
        this.environment = environment;

        if (isZos() && System.getProperty("ibm.system.encoding") != null) {
            this.encoding =
                    Charset.forName(System.getProperty("ibm.system.encoding")).toString();
        } else {
            this.encoding = Charset.defaultCharset().toString();
        }

        launcher = new LocalLauncher(IGitAPI.verbose ? listener : TaskListener.NULL);
    }

    /** {@inheritDoc} */
    @Override
    public GitClient subGit(String subdir) {
        return new CliGitAPIImpl(gitExe, new File(workspace, subdir), listener, environment);
    }

    /**
     * Initialize an empty repository for further git operations.
     *
     * @throws hudson.plugins.git.GitException if underlying git operation fails.
     * @throws java.lang.InterruptedException if interrupted.
     */
    @Override
    public void init() throws GitException, InterruptedException {
        init_().workspace(workspace.getAbsolutePath()).execute();
    }

    /**
     * Returns true if this workspace has a git repository.
     * Also returns true if this workspace contains an empty .git directory and
     * a parent directory has a git repository.
     *
     * @return true if this workspace has a git repository
     * @throws hudson.plugins.git.GitException if underlying git operation fails.
     * @throws java.lang.InterruptedException if interrupted.
     */
    @Override
    public boolean hasGitRepo() throws GitException, InterruptedException {
        if (hasGitRepo(".git")) {
            // Check if this is a valid git repo with --is-inside-work-tree
            try {
                launchCommand("rev-parse", "--is-inside-work-tree");
            } catch (Exception ex) {
                ex.printStackTrace(listener.error("Workspace has a .git repository, but it appears to be corrupt."));
                return false;
            }
            return true;
        }
        return false;
    }

    /**
     * Returns true if this workspace has a git repository.
     * If checkParentDirectories is true, searches parent directories.
     * If checkParentDirectories is false, checks workspace directory only.
     *
     * @param checkParentDirectories if true, search upward for a git repository
     * @return true if this workspace has a git repository
     * @throws hudson.plugins.git.GitException if underlying git operation fails.
     * @throws java.lang.InterruptedException if interrupted.
     */
    @Override
    public boolean hasGitRepo(boolean checkParentDirectories) throws GitException, InterruptedException {
        if (checkParentDirectories) {
            return hasGitRepo();
        }
        if (hasGitRepo(".git")) {
            // Check if this is a valid git repo with --resolve-git-dir
            try {
                launchCommand("rev-parse", "--resolve-git-dir", workspace.getAbsolutePath() + File.separator + ".git");
            } catch (Exception ex) {
                ex.printStackTrace(listener.error("Workspace has a .git repository, but it appears to be corrupt."));
                return false;
            }
            return true;
        }
        return false;
    }

    /**
     * Returns true if the parameter GIT_DIR is a directory which
     * contains a git repository.
     *
     * @param GIT_DIR a {@link java.lang.String} object.
     * @return true if GIT_DIR has a git repository
     * @throws hudson.plugins.git.GitException if underlying git operation fails.
     */
    public boolean hasGitRepo(String GIT_DIR) throws GitException {
        try {
            File dotGit = new File(workspace, GIT_DIR);
            return dotGit.exists();
        } catch (SecurityException ex) {
            throw new GitException(
                    "Security error when trying to check for .git. Are you sure you have correct permissions?", ex);
        } catch (Exception e) {
            throw new GitException("Couldn't check for .git", e);
        }
    }

    /** {@inheritDoc} */
    @Override
    public List<IndexEntry> getSubmodules(String treeIsh) throws GitException, InterruptedException {
        List<IndexEntry> submodules = lsTree(treeIsh, true);

        // Remove anything that isn't a submodule
        submodules.removeIf(indexEntry -> !indexEntry.getMode().equals("160000"));
        return submodules;
    }

    // Package protected for testing
    /**
     * Constant which disables safety check of remote URL.
     *
     * <code>CHECK_REMOTE_URL=Boolean.valueOf(System.getProperty(CliGitAPIImpl.class.getName() + ".checkRemoteURL", "true"))</code>.
     *
     * Refuse unsafe git URL's, including URL's that start with '-'
     * and URL's that contain space characters.
     *
     * Use '-Dorg.jenkinsci.plugins.gitclient.CliGitAPIImpl.checkRemoteURL=false'
     * to prevent check of remote URL.
     */
    static boolean CHECK_REMOTE_URL =
            Boolean.parseBoolean(System.getProperty(CliGitAPIImpl.class.getName() + ".checkRemoteURL", "true"));

    /**
     * SECURITY-1534 found that arguments
     * added to a git URL from the user interface can allow a user
     * with job creation permissions to execute an arbitrary program
     * on the git server if the git server is configured to allow
     * custom pack programs. Reject a URL if it includes invalid
     * content.
     */
    private void addCheckedRemoteUrl(@NonNull ArgumentListBuilder args, @NonNull String url) throws GitException {
        String trimmedUrl = url.trim();
        /* Don't check for invalid args if URL starts with known good cases.
         * Known good cases include:
         * '/' - Unix local file
         * 'C:' - Windows local file
         * 'file:', 'git:', 'http:', 'https:', 'ssh:', and 'git@' - known protocols
         */
        if (CHECK_REMOTE_URL
                && !trimmedUrl.startsWith("/")
                && !trimmedUrl.startsWith("\\\\")
                && !trimmedUrl.startsWith("file:")
                && !trimmedUrl.startsWith("git:")
                && !trimmedUrl.startsWith("git@")
                && !trimmedUrl.startsWith("http:")
                && !trimmedUrl.startsWith("https:")
                && !trimmedUrl.startsWith("ssh:")
                && !trimmedUrl.matches("^[A-Za-z]:.+")) {
            /* Not one of the known good cases, check if this could be a bad case
             * Bad cases include:
             * '-' - starts with 'dash' as possible argument
             * '`' - includes backquote in the string (command execution)
             * ' ' - includes a space (not guaranteed a threat, but threat risk increases)
             */
            if (trimmedUrl.startsWith("-")
                    || trimmedUrl.contains("`")
                    || trimmedUrl.contains("--upload-pack=")
                    || trimmedUrl.matches(".*\\s+.*")) {
                throw new GitException("Invalid remote URL: " + url);
            }
        }
        // Mark the end of options for git versions that support it.
        // Tells command line git that later arguments are operands, not options.
        // See POSIX 1-2017 guideline 10.
        if (isAtLeastVersion(2, 8, 0, 0)) {
            args.add("--"); // SECURITY-1534 - end of options, causes tests to fail with git 2.7.4 and older
        }
        args.add(trimmedUrl);
    }

    /**
     * fetch_.
     *
     * @return a {@link org.jenkinsci.plugins.gitclient.FetchCommand} object.
     */
    @Override
    public FetchCommand fetch_() {
        return new FetchCommand() {
            private URIish url;
            private List<RefSpec> refspecs;
            private boolean prune;
            private boolean shallow;
            private Integer timeout;
            private boolean tags = true;
            private Integer depth = 1;

            @Override
            public FetchCommand from(URIish remote, List<RefSpec> refspecs) {
                this.url = remote;
                this.refspecs = refspecs;
                return this;
            }

            @Override
            public FetchCommand tags(boolean tags) {
                this.tags = tags;
                return this;
            }

            @Deprecated
            @Override
            public FetchCommand prune() {
                return prune(true);
            }

            @Override
            public FetchCommand prune(boolean prune) {
                this.prune = prune;
                return this;
            }

            @Override
            public FetchCommand shallow(boolean shallow) {
                this.shallow = shallow;
                return this;
            }

            @Override
            public FetchCommand timeout(Integer timeout) {
                this.timeout = timeout;
                return this;
            }

            @Override
            public FetchCommand depth(Integer depth) {
                this.depth = depth;
                return this;
            }

            @Override
            public void execute() throws GitException, InterruptedException {
                listener.getLogger().println("Fetching upstream changes from " + url);

                ArgumentListBuilder args = new ArgumentListBuilder();
                args.add("fetch");
                args.add(tags ? "--tags" : "--no-tags");
                if (USE_FORCE_FETCH && isAtLeastVersion(2, 20, 0, 0)) {
                    /* CLI git 2.20.0 fixed a long-standing bug that now requires --force to update existing tags */
                    args.add("--force");
                }
                if (isAtLeastVersion(1, 7, 1, 0)) {
                    args.add("--progress");
                }

                if (prune) {
                    args.add("--prune");
                }

                if (shallow) {
                    if (depth == null) {
                        depth = 1;
                    }
                    args.add("--depth=" + depth);
                }

                warnIfWindowsTemporaryDirNameHasSpaces();

                StandardCredentials cred = credentials.get(url.toPrivateString());
                if (cred == null) {
                    cred = defaultCredentials;
                }
                if (isAtLeastVersion(1, 8, 0, 0)) {
                    addCheckedRemoteUrl(args, url.toPrivateASCIIString());
                } else {
                    // CLI git 1.7.1 on CentOS 6 rejects URL encoded
                    // repo URL. This is how git client behaved before
                    // 2.8.5, with the addition of a safety check for
                    // the remote URL string contents.
                    //
                    // CLI git 1.7.1 is unsupported by the git client
                    // plugin, but we try to avoid removing
                    // capabilities that worked previously.
                    addCheckedRemoteUrl(args, url.toString());
                }

                if (refspecs != null) {
                    for (RefSpec rs : refspecs) {
                        if (rs != null) {
                            args.add(rs.toString());
                        }
                    }
                }

                /* If url looks like a remote name reference, convert to remote URL for authentication */
                /* See JENKINS-50573 for more details */
                /* "git remote add" rejects remote names with ':' (and it is a common character in remote URLs) */
                /* "git remote add" allows remote names with '@' but internal git parsing problems seem likely (and it is a common character in remote URLs) */
                /* "git remote add" allows remote names with '/' but git client plugin parsing problems will occur (and it is a common character in remote URLs) */
                /* "git remote add" allows remote names with '\' but git client plugin parsing problems will occur */
                URIish remoteUrl = url;
                if (!url.isRemote() && !StringUtils.containsAny(url.toString(), ":@/\\")) {
                    try {
                        remoteUrl = new URIish(getRemoteUrl(url.toString()));
                    } catch (URISyntaxException e) {
                        listener.getLogger().println("Unexpected remote name or URL: '" + url + "'");
                    }
                }

                launchCommandWithCredentials(args, workspace, cred, remoteUrl, timeout);
            }
        };
    }

    /** {@inheritDoc} */
    @Deprecated
    @Override
    public void fetch(URIish url, List<RefSpec> refspecs) throws GitException, InterruptedException {
        fetch_().from(url, refspecs).execute();
    }

    /** {@inheritDoc} */
    @Deprecated
    @Override
    public void fetch(String remoteName, RefSpec... refspec) throws GitException, InterruptedException {
        listener.getLogger().println("Fetching upstream changes" + (remoteName != null ? " from " + remoteName : ""));

        ArgumentListBuilder args = new ArgumentListBuilder();
        args.add("fetch", "-t");
        if (USE_FORCE_FETCH && isAtLeastVersion(2, 20, 0, 0)) {
            /* CLI git 2.20.0 fixed a long-standing bug that now requires --force to update existing tags */
            args.add("--force");
        }

        if (remoteName == null) {
            remoteName = getDefaultRemote();
        }

        String url = getRemoteUrl(remoteName);
        if (url == null) {
            throw new GitException("remote." + remoteName + ".url not defined");
        }
        addCheckedRemoteUrl(args, url);
        if (refspec != null && refspec.length > 0) {
            for (RefSpec rs : refspec) {
                if (rs != null) {
                    args.add(rs.toString());
                }
            }
        }

        StandardCredentials cred = credentials.get(url);
        if (cred == null) {
            cred = defaultCredentials;
        }
        launchCommandWithCredentials(args, workspace, cred, url);
    }

    /** {@inheritDoc} */
    @Deprecated
    @Override
    public void fetch(String remoteName, RefSpec refspec) throws GitException, InterruptedException {
        fetch(remoteName, new RefSpec[] {refspec});
    }

    /** {@inheritDoc} */
    @Override
    public void reset(boolean hard) throws GitException, InterruptedException {
        try {
            validateRevision("HEAD");
        } catch (GitException e) {
            listener.getLogger().println("No valid HEAD. Skipping the resetting");
            return;
        }
        listener.getLogger().println("Resetting working tree");

        ArgumentListBuilder args = new ArgumentListBuilder();
        args.add("reset");
        if (hard) {
            args.add("--hard");
        }

        launchCommand(args);
    }

    /**
     * clone_.
     *
     * @return a {@link org.jenkinsci.plugins.gitclient.CloneCommand} object.
     */
    @Override
    public CloneCommand clone_() {
        return new CloneCommand() {
            private String url;
            private String origin = "origin";
            private String reference;
            private boolean shallow, shared;
            private Integer timeout;
            private boolean tags = true;
            private List<RefSpec> refspecs;
            private Integer depth = 1;

            @Override
            public CloneCommand url(String url) {
                this.url = url;
                return this;
            }

            @Override
            public CloneCommand repositoryName(String name) {
                this.origin = name;
                return this;
            }

            @Deprecated
            @Override
            public CloneCommand shared() {
                return shared(true);
            }

            @Override
            public CloneCommand shared(boolean shared) {
                this.shared = shared;
                return this;
            }

            @Deprecated
            @Override
            public CloneCommand shallow() {
                return shallow(true);
            }

            @Override
            public CloneCommand shallow(boolean shallow) {
                this.shallow = shallow;
                return this;
            }

            @Deprecated
            @Override
            public CloneCommand noCheckout() {
                // this.noCheckout = true; Since the "clone" command has been replaced with init + fetch, the
                // --no-checkout option is always satisfied
                return this;
            }

            @Override
            public CloneCommand tags(boolean tags) {
                this.tags = tags;
                return this;
            }

            @Override
            public CloneCommand reference(String reference) {
                this.reference = reference;
                return this;
            }

            @Override
            public CloneCommand timeout(Integer timeout) {
                this.timeout = timeout;
                return this;
            }

            @Override
            public CloneCommand depth(Integer depth) {
                this.depth = depth;
                return this;
            }

            @Override
            public CloneCommand refspecs(List<RefSpec> refspecs) {
                this.refspecs = new ArrayList<>(refspecs);
                return this;
            }

            @Override
            public void execute() throws GitException, InterruptedException {

                URIish urIish = null;
                try {
                    urIish = new URIish(url);
                } catch (URISyntaxException e) {
                    listener.getLogger().println("Invalid repository " + url);
                    throw new IllegalArgumentException("Invalid repository " + url, e);
                }

                listener.getLogger().println("Cloning repository " + url);

                try {
                    Util.deleteContentsRecursive(workspace);
                } catch (Exception e) {
                    e.printStackTrace(listener.error("Failed to clean the workspace"));
                    throw new GitException("Failed to delete workspace", e);
                }

                // we don't run a 'git clone' command but git init + git fetch
