jetbrains.buildServer.buildTriggers.vcs.git.agent.UpdaterImpl.java Source code

Java tutorial

Introduction

Here is the source code for jetbrains.buildServer.buildTriggers.vcs.git.agent.UpdaterImpl.java

Source

/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jetbrains.buildServer.buildTriggers.vcs.git.agent;

import com.intellij.openapi.util.Trinity;
import jetbrains.buildServer.agent.AgentRunningBuild;
import jetbrains.buildServer.agent.BuildDirectoryCleanerCallback;
import jetbrains.buildServer.agent.BuildProgressLogger;
import jetbrains.buildServer.agent.SmartDirectoryCleaner;
import jetbrains.buildServer.buildTriggers.vcs.git.*;
import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.*;
import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.impl.CommandUtil;
import jetbrains.buildServer.buildTriggers.vcs.git.agent.command.impl.RefImpl;
import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitExecTimeout;
import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitIndexCorruptedException;
import jetbrains.buildServer.buildTriggers.vcs.git.agent.errors.GitOutdatedIndexException;
import jetbrains.buildServer.log.Loggers;
import jetbrains.buildServer.util.FileUtil;
import jetbrains.buildServer.vcs.*;
import org.apache.log4j.Logger;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.*;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.transport.URIish;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;

import static com.intellij.openapi.util.text.StringUtil.isEmpty;
import static jetbrains.buildServer.buildTriggers.vcs.git.GitUtils.*;

public class UpdaterImpl implements Updater {

    private final static Logger LOG = Logger.getLogger(UpdaterImpl.class);
    /** Git version which supports --progress option in the fetch command */
    private final static GitVersion GIT_WITH_PROGRESS_VERSION = new GitVersion(1, 7, 1, 0);
    //--force option in git submodule update introduced in 1.7.6
    private final static GitVersion GIT_WITH_FORCE_SUBMODULE_UPDATE = new GitVersion(1, 7, 6);
    public final static GitVersion GIT_WITH_SPARSE_CHECKOUT = new GitVersion(1, 7, 4);
    public final static GitVersion BROKEN_SPARSE_CHECKOUT = new GitVersion(2, 7, 0);
    /**
     * Git version supporting an empty credential helper - the only way to disable system/global/local cred helper
     */
    public final static GitVersion EMPTY_CRED_HELPER = new GitVersion(2, 9, 0);
    private static final int SILENT_TIMEOUT = 24 * 60 * 60; //24 hours

    protected final FS myFS;
    private final SmartDirectoryCleaner myDirectoryCleaner;
    protected final BuildProgressLogger myLogger;
    protected final AgentPluginConfig myPluginConfig;
    protected final GitFactory myGitFactory;
    protected final File myTargetDirectory;
    protected final String myRevision;
    protected final AgentGitVcsRoot myRoot;
    protected final String myFullBranchName;
    protected final AgentRunningBuild myBuild;
    private final CheckoutRules myRules;
    private final CheckoutMode myCheckoutMode;
    protected final MirrorManager myMirrorManager;

    public UpdaterImpl(@NotNull FS fs, @NotNull AgentPluginConfig pluginConfig,
            @NotNull MirrorManager mirrorManager, @NotNull SmartDirectoryCleaner directoryCleaner,
            @NotNull GitFactory gitFactory, @NotNull AgentRunningBuild build, @NotNull VcsRoot root,
            @NotNull String version, @NotNull File targetDir, @NotNull CheckoutRules rules,
            @NotNull CheckoutMode checkoutMode) throws VcsException {
        myFS = fs;
        myPluginConfig = pluginConfig;
        myDirectoryCleaner = directoryCleaner;
        myGitFactory = gitFactory;
        myBuild = build;
        myLogger = build.getBuildLogger();
        myRevision = GitUtils.versionRevision(version);
        myTargetDirectory = targetDir;
        myRoot = new AgentGitVcsRoot(mirrorManager, myTargetDirectory, root);
        myFullBranchName = getBranch();
        myRules = rules;
        myCheckoutMode = checkoutMode;
        myMirrorManager = mirrorManager;
    }

    private String getBranch() {
        String defaultBranchName = GitUtils.expandRef(myRoot.getRef());
        String rootBranchParam = GitUtils.getGitRootBranchParamName(myRoot.getOriginalRoot());
        String customBranch = myBuild.getSharedConfigParameters().get(rootBranchParam);
        return customBranch != null ? customBranch : defaultBranchName;
    }

    public void update() throws VcsException {
        myLogger.message("Git version: " + myPluginConfig.getGitVersion());
        checkAuthMethodIsSupported();
        doUpdate();
    }

    protected void doUpdate() throws VcsException {
        logStartUpdating();
        initGitRepository();
        removeRefLocks(new File(myTargetDirectory, ".git"));
        doFetch();
        updateSources();
    }

    private void logStartUpdating() {
        LOG.info("Starting update of root " + myRoot.getName() + " in " + myTargetDirectory + " to revision "
                + myRevision);
        LOG.debug("Updating " + myRoot.debugInfo());
    }

    /**
     * Init .git in the target dir
     * @return true if there was no fetch in the target dir before
     * @throws VcsException in teh case of any problems
     */
    private boolean initGitRepository() throws VcsException {
        boolean firstFetch = false;
        if (!new File(myTargetDirectory, ".git").exists()) {
            initDirectory();
            firstFetch = true;
        } else {
            String remoteUrl = getRemoteUrl();
            if (!remoteUrl.equals(myRoot.getRepositoryFetchURL().toString())) {
                initDirectory();
                firstFetch = true;
            } else {
                try {
                    setupExistingRepository();
                    configureSparseCheckout();
                } catch (Exception e) {
                    LOG.warn("Do clean checkout due to errors while configure use of local mirrors", e);
                    initDirectory();
                    firstFetch = true;
                }
            }
        }
        return firstFetch;
    }

    protected void setupNewRepository() throws VcsException {
    }

    protected void setupExistingRepository() throws VcsException {
        removeUrlSections();
        disableAlternates();
    }

    private void updateSources() throws VcsException {
        final GitFacade git = myGitFactory.create(myTargetDirectory);
        boolean branchChanged = false;
        removeIndexLock();
        if (isRegularBranch(myFullBranchName)) {
            String branchName = getShortBranchName(myFullBranchName);
            Branches branches = git.listBranches();
            if (branches.isCurrentBranch(branchName)) {
                removeIndexLock();
                runAndFixIndexErrors(git, new VcsCommand() {
                    @Override
                    public void call() throws VcsException {
                        reset(git).setHard(true).setRevision(myRevision).call();
                    }
                });
                git.setUpstream(branchName, GitUtils.createRemoteRef(myFullBranchName)).call();
            } else {
                branchChanged = true;
                if (!branches.contains(branchName)) {
                    git.createBranch().setName(branchName).setStartPoint(GitUtils.createRemoteRef(myFullBranchName))
                            .setTrack(true).call();
                }
                git.updateRef().setRef(myFullBranchName).setRevision(myRevision).call();
                final String finalBranchName = branchName;
                runAndFixIndexErrors(git, new VcsCommand() {
                    @Override
                    public void call() throws VcsException {
                        checkout(git).setForce(true).setBranch(finalBranchName)
                                .setTimeout(myPluginConfig.getCheckoutIdleTimeoutSeconds()).call();
                    }
                });
                if (branches.contains(branchName)) {
                    git.setUpstream(branchName, GitUtils.createRemoteRef(myFullBranchName)).call();
                }
            }
        } else if (isTag(myFullBranchName)) {
            final String shortName = myFullBranchName.substring("refs/tags/".length());
            runAndFixIndexErrors(git, new VcsCommand() {
                @Override
                public void call() throws VcsException {
                    checkout(git).setForce(true).setBranch(shortName)
                            .setTimeout(myPluginConfig.getCheckoutIdleTimeoutSeconds()).call();
                }
            });
            Ref tag = getRef(myTargetDirectory, myFullBranchName);
            if (tag != null && !tag.getObjectId().name().equals(myRevision)) {
                runAndFixIndexErrors(git, new VcsCommand() {
                    @Override
                    public void call() throws VcsException {
                        checkout(git).setBranch(myRevision).setForce(true)
                                .setTimeout(myPluginConfig.getCheckoutIdleTimeoutSeconds()).call();
                    }
                });
            }
            branchChanged = true;
        } else {
            runAndFixIndexErrors(git, new VcsCommand() {
                @Override
                public void call() throws VcsException {
                    checkout(git).setForce(true).setBranch(myRevision)
                            .setTimeout(myPluginConfig.getCheckoutIdleTimeoutSeconds()).call();
                }
            });
            branchChanged = true;
        }

        doClean(branchChanged);
        if (myRoot.isCheckoutSubmodules()) {
            checkoutSubmodules(myTargetDirectory);
        }
    }

    private void runAndFixIndexErrors(@NotNull GitFacade git, @NotNull VcsCommand cmd) throws VcsException {
        try {
            cmd.call();
        } catch (GitIndexCorruptedException e) {
            File gitIndex = e.getGitIndex();
            myLogger.message("Git index '" + gitIndex.getAbsolutePath()
                    + "' is corrupted, remove it and repeat the command");
            FileUtil.delete(gitIndex);
            cmd.call();
        } catch (GitOutdatedIndexException e) {
            myLogger.message("Refresh outdated git index and repeat the command");
            updateIndex(git).reallyRefresh(true).quiet(true).call();
            cmd.call();
        } catch (Exception e) {
            if (e instanceof VcsException)
                throw (VcsException) e;
            throw new VcsException(e);
        }
    }

    @NotNull
    private UpdateIndexCommand updateIndex(final GitFacade git) {
        UpdateIndexCommand result = git.updateIndex().setAuthSettings(myRoot.getAuthSettings())
                .setUseNativeSsh(myPluginConfig.isUseNativeSSH());
        configureLFS(result);
        return result;
    }

    @NotNull
    private ResetCommand reset(final GitFacade git) {
        ResetCommand result = git.reset().setAuthSettings(myRoot.getAuthSettings())
                .setUseNativeSsh(myPluginConfig.isUseNativeSSH());
        configureLFS(result);
        return result;
    }

    @NotNull
    private CheckoutCommand checkout(final GitFacade git) {
        CheckoutCommand result = git.checkout().setAuthSettings(myRoot.getAuthSettings())
                .setUseNativeSsh(myPluginConfig.isUseNativeSSH());
        configureLFS(result);
        return result;
    }

    private void checkoutSubmodules(@NotNull final File repositoryDir) throws VcsException {
        File dotGitModules = new File(repositoryDir, ".gitmodules");
        try {
            Config gitModules = readGitModules(dotGitModules);
            if (gitModules == null)
                return;

            myLogger.message("Checkout submodules in " + repositoryDir);
            GitFacade git = myGitFactory.create(repositoryDir);
            git.submoduleInit().call();
            git.submoduleSync().call();

            addSubmoduleUsernames(repositoryDir, gitModules);

            long start = System.currentTimeMillis();
            git.submoduleUpdate().setAuthSettings(myRoot.getAuthSettings())
                    .setUseNativeSsh(myPluginConfig.isUseNativeSSH()).setTimeout(SILENT_TIMEOUT)
                    .setForce(isForceUpdateSupported()).call();

            if (recursiveSubmoduleCheckout()) {
                for (String submodulePath : getSubmodulePaths(gitModules)) {
                    checkoutSubmodules(new File(repositoryDir, submodulePath));
                }
            }
            Loggers.VCS.info("Submodules update in " + repositoryDir.getAbsolutePath() + " is finished in "
                    + (System.currentTimeMillis() - start) + " ms");

        } catch (IOException e) {
            Loggers.VCS.error("Submodules checkout failed", e);
            throw new VcsException("Submodules checkout failed", e);
        } catch (ConfigInvalidException e) {
            Loggers.VCS.error("Submodules checkout failed", e);
            throw new VcsException("Submodules checkout failed", e);
        }
    }

    private boolean isForceUpdateSupported() {
        return !GIT_WITH_FORCE_SUBMODULE_UPDATE.isGreaterThan(myPluginConfig.getGitVersion());
    }

    private void addSubmoduleUsernames(@NotNull File repositoryDir, @NotNull Config gitModules)
            throws IOException, ConfigInvalidException, VcsException {
        if (!myPluginConfig.isUseMainRepoUserForSubmodules())
            return;

        Loggers.VCS.info("Update submodules credentials");

        AuthSettings auth = myRoot.getAuthSettings();
        final String userName = auth.getUserName();
        if (userName == null) {
            Loggers.VCS.info("Username is not specified in the main VCS root settings, skip updating credentials");
            return;
        }

        Repository r = new RepositoryBuilder().setBare().setGitDir(getGitDir(repositoryDir)).build();
        StoredConfig gitConfig = r.getConfig();

        Set<String> submodules = gitModules.getSubsections("submodule");
        if (submodules.isEmpty()) {
            Loggers.VCS.info("No submodule sections found in "
                    + new File(repositoryDir, ".gitmodules").getCanonicalPath() + ", skip updating credentials");
            return;
        }
        File modulesDir = new File(r.getDirectory(), Constants.MODULES);
        for (String submoduleName : submodules) {
            String url = gitModules.getString("submodule", submoduleName, "url");
            Loggers.VCS.info("Update credentials for submodule with url " + url);
            if (url == null || !isRequireAuth(url)) {
                Loggers.VCS.info("Url " + url + " does not require authentication, skip updating credentials");
                continue;
            }
            try {
                URIish uri = new URIish(url);
                String updatedUrl = uri.setUser(userName).toASCIIString();
                gitConfig.setString("submodule", submoduleName, "url", updatedUrl);
                String submodulePath = gitModules.getString("submodule", submoduleName, "path");
                if (submodulePath != null && myPluginConfig.isUpdateSubmoduleOriginUrl()) {
                    File submoduleDir = new File(modulesDir, submodulePath);
                    if (submoduleDir.isDirectory() && new File(submoduleDir, Constants.CONFIG).isFile())
                        updateOriginUrl(submoduleDir, updatedUrl);
                }
                Loggers.VCS.debug("Submodule url " + url + " changed to " + updatedUrl);
            } catch (URISyntaxException e) {
                Loggers.VCS.warn("Error while parsing an url " + url + ", skip updating submodule credentials", e);
            } catch (Exception e) {
                Loggers.VCS.warn("Error while updating the '" + submoduleName + "' submodule url", e);
            }
        }
        gitConfig.save();
    }

    private void updateOriginUrl(@NotNull File repoDir, @NotNull String url) throws IOException {
        Repository r = new RepositoryBuilder().setBare().setGitDir(repoDir).build();
        StoredConfig config = r.getConfig();
        config.setString("remote", "origin", "url", url);
        config.save();
    }

    @Nullable
    private Config readGitModules(@NotNull File dotGitModules) throws IOException, ConfigInvalidException {
        if (!dotGitModules.exists())
            return null;
        String content = FileUtil.readText(dotGitModules);
        Config config = new Config();
        config.fromText(content);
        return config;
    }

    private boolean isRequireAuth(@NotNull String url) {
        try {
            URIish uri = new URIish(url);
            String scheme = uri.getScheme();
            if (scheme == null || "git".equals(scheme)) //no auth for anonymous protocol and for local repositories
                return false;
            String user = uri.getUser();
            if (user != null) //respect a user specified in config
                return false;
            return true;
        } catch (URISyntaxException e) {
            return false;
        }
    }

    private Set<String> getSubmodulePaths(@NotNull Config config) {
        Set<String> paths = new HashSet<String>();
        Set<String> submodules = config.getSubsections("submodule");
        for (String submoduleName : submodules) {
            String submodulePath = config.getString("submodule", submoduleName, "path");
            paths.add(submodulePath.replaceAll("/", Matcher.quoteReplacement(File.separator)));
        }
        return paths;
    }

    private boolean recursiveSubmoduleCheckout() {
        return SubmodulesCheckoutPolicy.CHECKOUT.equals(myRoot.getSubmodulesCheckoutPolicy())
                || SubmodulesCheckoutPolicy.CHECKOUT_IGNORING_ERRORS.equals(myRoot.getSubmodulesCheckoutPolicy());
    }

    private void doClean(boolean branchChanged) throws VcsException {
        if (myRoot.getCleanPolicy() == AgentCleanPolicy.ALWAYS
                || branchChanged && myRoot.getCleanPolicy() == AgentCleanPolicy.ON_BRANCH_CHANGE) {
            myLogger.message("Cleaning " + myRoot.getName() + " in " + myTargetDirectory + " the file set "
                    + myRoot.getCleanFilesPolicy());
            myGitFactory.create(myTargetDirectory).clean().setCleanPolicy(myRoot.getCleanFilesPolicy()).call();

            if (myRoot.isCheckoutSubmodules())
                cleanSubmodules(myTargetDirectory);
        }
    }

    private void cleanSubmodules(@NotNull File repositoryDir) throws VcsException {
        File dotGitModules = new File(repositoryDir, ".gitmodules");
        Config gitModules;
        try {
            gitModules = readGitModules(dotGitModules);
        } catch (Exception e) {
            Loggers.VCS.error("Error while reading " + dotGitModules.getAbsolutePath() + ": " + e.getMessage());
            throw new VcsException("Error while reading " + dotGitModules.getAbsolutePath(), e);
        }

        if (gitModules == null)
            return;

        for (String submodulePath : getSubmodulePaths(gitModules)) {
            File submoduleDir = new File(repositoryDir, submodulePath);
            try {
                myLogger.message(
                        "Cleaning files in " + submoduleDir + " the file set " + myRoot.getCleanFilesPolicy());
                myGitFactory.create(submoduleDir).clean().setCleanPolicy(myRoot.getCleanFilesPolicy()).call();
            } catch (Exception e) {
                Loggers.VCS.error("Error while cleaning files in " + submoduleDir.getAbsolutePath(), e);
            }
            if (recursiveSubmoduleCheckout())
                cleanSubmodules(submoduleDir);
        }
    }

    protected void removeUrlSections() throws VcsException {
        Repository r = null;
        try {
            r = new RepositoryBuilder().setWorkTree(myTargetDirectory).build();
            StoredConfig config = r.getConfig();
            Set<String> urlSubsections = config.getSubsections("url");
            for (String subsection : urlSubsections) {
                config.unsetSection("url", subsection);
            }
            config.save();
        } catch (IOException e) {
            String msg = "Error while remove url.* sections";
            LOG.error(msg, e);
            throw new VcsException(msg, e);
        } finally {
            if (r != null)
                r.close();
        }
    }

    protected void disableAlternates() {
        FileUtil.delete(new File(myTargetDirectory,
                ".git" + File.separator + "objects" + File.separator + "info" + File.separator + "alternates"));
    }

    private String getRemoteUrl() {
        try {
            return myGitFactory.create(myTargetDirectory).getConfig().setPropertyName("remote.origin.url").call();
        } catch (VcsException e) {
            LOG.debug("Failed to read property", e);
            return "";
        }
    }

    @Nullable
    protected Ref getRef(@NotNull File repositoryDir, @NotNull String ref) {
        Map<String, Ref> refs = myGitFactory.create(repositoryDir).showRef().setPattern(ref).call().getValidRefs();
        return refs.isEmpty() ? null : refs.get(ref);
    }

    /**
     * If some git process crashed in this repository earlier it can leave lock files for index.
     * This method delete such lock file if it exists (with warning message), otherwise git operation will fail.
     */
    private void removeIndexLock() {
        File indexLock = new File(myTargetDirectory, ".git" + File.separator + "index.lock");
        if (indexLock.exists()) {
            myLogger.warning(
                    "The .git/index.lock file exists. This probably means a git process crashed in this repository earlier. Deleting lock file");
            FileUtil.delete(indexLock);
        }
    }

    private void doFetch() throws VcsException {
        boolean outdatedRefsFound = removeOutdatedRefs(myTargetDirectory);
        ensureCommitLoaded(outdatedRefsFound);
    }

    protected void ensureCommitLoaded(boolean fetchRequired) throws VcsException {
        fetchFromOriginalRepository(fetchRequired);
    }

    protected void fetchFromOriginalRepository(boolean fetchRequired) throws VcsException {
        if (myPluginConfig.isFetchAllHeads()) {
            String msg = getForcedHeadsFetchMessage();
            LOG.info(msg);
            myLogger.message(msg);

            fetchAllBranches();
            if (!myFullBranchName.startsWith("refs/heads/")) {
                Ref remoteRef = getRef(myTargetDirectory, GitUtils.createRemoteRef(myFullBranchName));
                if (!fetchRequired && remoteRef != null && myRevision.equals(remoteRef.getObjectId().name())
                        && hasRevision(myTargetDirectory, myRevision))
                    return;
            }
        } else {
            Ref remoteRef = getRef(myTargetDirectory, GitUtils.createRemoteRef(myFullBranchName));
            if (!fetchRequired && remoteRef != null && myRevision.equals(remoteRef.getObjectId().name())
                    && hasRevision(myTargetDirectory, myRevision))
                return;
            myLogger.message("Commit '" + myRevision + "' is not found in local clone. Running 'git fetch'...");
            fetchDefaultBranch();
            if (hasRevision(myTargetDirectory, myRevision))
                return;
            myLogger.message("Commit still not found after fetching main branch. Fetching more branches.");
            fetchAllBranches();
        }
        if (hasRevision(myTargetDirectory, myRevision))
            return;
        throw new VcsException("Cannot find commit " + myRevision);
    }

    protected String getForcedHeadsFetchMessage() {
        return "Forced fetch of all heads (" + PluginConfigImpl.FETCH_ALL_HEADS + "="
                + myBuild.getSharedConfigParameters().get(PluginConfigImpl.FETCH_ALL_HEADS) + ")";
    }

    private void fetchDefaultBranch() throws VcsException {
        fetch(myTargetDirectory, getRefspecForFetch(), false);
    }

    private String getRefspecForFetch() {
        return "+" + myFullBranchName + ":" + GitUtils.createRemoteRef(myFullBranchName);
    }

    private void fetchAllBranches() throws VcsException {
        fetch(myTargetDirectory, "+refs/heads/*:refs/remotes/origin/*", false);
    }

    protected boolean hasRevision(@NotNull File repositoryDir, @NotNull String revision) {
        return getRevision(repositoryDir, revision) != null;
    }

    private String getRevision(@NotNull File repositoryDir, @NotNull String revision) {
        return myGitFactory.create(repositoryDir).log().setCommitsNumber(1).setPrettyFormat("%H%x20%s")
                .setStartPoint(revision).call();
    }

    protected void fetch(@NotNull File repositoryDir, @NotNull String refspec, boolean shallowClone)
            throws VcsException {
        boolean silent = isSilentFetch();
        int timeout = getTimeout(silent);

        try {
            getFetch(repositoryDir, refspec, shallowClone, silent, timeout).call();
        } catch (GitIndexCorruptedException e) {
            File gitIndex = e.getGitIndex();
            myLogger.message(
                    "Git index '" + gitIndex.getAbsolutePath() + "' is corrupted, remove it and repeat git fetch");
            FileUtil.delete(gitIndex);
            getFetch(repositoryDir, refspec, shallowClone, silent, timeout).call();
        } catch (GitExecTimeout e) {
            if (!silent) {
                myLogger.error("No output from git during " + timeout
                        + " seconds. Try increasing idle timeout by setting parameter '"
                        + PluginConfigImpl.IDLE_TIMEOUT + "' either in build or in agent configuration.");
            }
            throw e;
        }
    }

    @NotNull
    private FetchCommand getFetch(@NotNull File repositoryDir, @NotNull String refspec, boolean shallowClone,
            boolean silent, int timeout) {
        FetchCommand result = myGitFactory.create(repositoryDir).fetch().setAuthSettings(myRoot.getAuthSettings())
                .setUseNativeSsh(myPluginConfig.isUseNativeSSH()).setTimeout(timeout).setRefspec(refspec);

        if (silent)
            result.setQuite(true);
        else
            result.setShowProgress(true);

        if (shallowClone)
            result.setDepth(1);

        return result;
    }

    protected void removeRefLocks(@NotNull File dotGit) {
        File refs = new File(dotGit, "refs");
        if (!refs.isDirectory())
            return;
        Collection<File> locks = FileUtil.findFiles(new FileFilter() {
            public boolean accept(File f) {
                return f.isFile() && f.getName().endsWith(".lock");
            }
        }, refs);
        for (File lock : locks) {
            LOG.info("Remove a lock file " + lock.getAbsolutePath());
            FileUtil.delete(lock);
        }
    }

    private boolean isSilentFetch() {
        GitVersion version = myPluginConfig.getGitVersion();
        return version.isLessThan(GIT_WITH_PROGRESS_VERSION);
    }

    private int getTimeout(boolean silentFetch) {
        if (silentFetch)
            return SILENT_TIMEOUT;
        else
            return myPluginConfig.getIdleTimeoutSeconds();
    }

    private void checkAuthMethodIsSupported() throws VcsException {
        checkAuthMethodIsSupported(myRoot, myPluginConfig);
    }

    static void checkAuthMethodIsSupported(@NotNull GitVcsRoot root, @NotNull AgentPluginConfig config)
            throws VcsException {
        if ("git".equals(root.getRepositoryFetchURL().getScheme()))
            return;//anonymous protocol, don't check anything
        AuthSettings authSettings = root.getAuthSettings();
        switch (authSettings.getAuthMethod()) {
        case PASSWORD:
            if ("http".equals(root.getRepositoryFetchURL().getScheme())
                    || "https".equals(root.getRepositoryFetchURL().getScheme())) {
                GitVersion actualVersion = config.getGitVersion();
                GitVersion requiredVersion = getMinVersionForHttpAuth();
                if (actualVersion.isLessThan(requiredVersion)) {
                    throw new VcsException(
                            "Password authentication requires git " + requiredVersion + ", found git version is "
                                    + actualVersion + ". Upgrade git or use different authentication method.");
                }
            } else {
                throw new VcsException("TeamCity doesn't support authentication method '"
                        + root.getAuthSettings().getAuthMethod().uiName()
                        + "' with agent checkout and non-http protocols. Please use different authentication method.");
            }
            break;
        case PRIVATE_KEY_FILE:
            throw new VcsException("TeamCity doesn't support authentication method '"
                    + root.getAuthSettings().getAuthMethod().uiName()
                    + "' with agent checkout. Please use different authentication method.");
        }
    }

    @NotNull
    private static GitVersion getMinVersionForHttpAuth() {
        //core.askpass parameter was added in 1.7.1, but
        //experiments show that it works only in 1.7.3 on linux
        //and msysgit 1.7.3.1-preview20101002.
        return new GitVersion(1, 7, 3);
    }

    /**
     * Clean and init directory and configure remote origin
     *
     * @throws VcsException if there are problems with initializing the directory
     */
    void initDirectory() throws VcsException {
        BuildDirectoryCleanerCallback c = new BuildDirectoryCleanerCallback(myLogger, LOG);
        myDirectoryCleaner.cleanFolder(myTargetDirectory, c);
        //noinspection ResultOfMethodCallIgnored
        myTargetDirectory.mkdirs();
        if (c.isHasErrors()) {
            throw new VcsException(
                    "Unable to clean directory " + myTargetDirectory + " for VCS root " + myRoot.getName());
        }
        myLogger.message("The .git directory is missing in '" + myTargetDirectory + "'. Running 'git init'...");
        myGitFactory.create(myTargetDirectory).init().call();
        validateUrls();
        myGitFactory.create(myRoot.getLocalRepositoryDir()).addRemote().setName("origin")
                .setUrl(myRoot.getRepositoryFetchURL().toString()).call();
        URIish url = myRoot.getRepositoryPushURL();
        String pushUrl = url == null ? null : url.toString();
        if (pushUrl != null && !pushUrl.equals(myRoot.getRepositoryFetchURL().toString())) {
            myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("remote.origin.pushurl")
                    .setValue(pushUrl).call();
        }
        setupNewRepository();
        configureSparseCheckout();
    }

    private void configureSparseCheckout() throws VcsException {
        if (myCheckoutMode == CheckoutMode.SPARSE_CHECKOUT) {
            setupSparseCheckout();
        } else {
            myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("core.sparseCheckout")
                    .setValue("false").call();
        }
    }

    private void setupSparseCheckout() throws VcsException {
        myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("core.sparseCheckout").setValue("true")
                .call();
        File sparseCheckout = new File(myTargetDirectory, ".git/info/sparse-checkout");
        boolean hasIncludeRules = false;
        StringBuilder sparseCheckoutContent = new StringBuilder();
        for (IncludeRule rule : myRules.getIncludeRules()) {
            if (isEmpty(rule.getFrom())) {
                sparseCheckoutContent.append("/*\n");
            } else {
                sparseCheckoutContent.append("/").append(rule.getFrom()).append("\n");
            }
            hasIncludeRules = true;
        }
        if (!hasIncludeRules) {
            sparseCheckoutContent.append("/*\n");
        }
        for (FileRule rule : myRules.getExcludeRules()) {
            sparseCheckoutContent.append("!/").append(rule.getFrom()).append("\n");
        }
        try {
            FileUtil.writeFileAndReportErrors(sparseCheckout, sparseCheckoutContent.toString());
        } catch (IOException e) {
            LOG.warn("Error while writing sparse checkout config, disable sparse checkout", e);
            myGitFactory.create(myTargetDirectory).setConfig().setPropertyName("core.sparseCheckout")
                    .setValue("false").call();
        }
    }

    private void validateUrls() {
        URIish fetch = myRoot.getRepositoryFetchURL();
        if (isAnonymousGitWithUsername(fetch))
            LOG.warn("Fetch URL '" + fetch.toString() + "' for root " + myRoot.getName()
                    + " uses an anonymous git protocol and contains a username, fetch will probably fail");
        URIish push = myRoot.getRepositoryPushURL();
        if (!fetch.equals(push) && isAnonymousGitWithUsername(push))
            LOG.warn("Push URL '" + push.toString() + "'for root " + myRoot.getName()
                    + " uses an anonymous git protocol and contains a username, push will probably fail");
    }

    protected boolean removeOutdatedRefs(@NotNull File workingDir) throws VcsException {
        boolean outdatedRefsRemoved = false;
        GitFacade git = myGitFactory.create(workingDir);
        ShowRefResult showRefResult = git.showRef().call();
        Refs localRefs = new Refs(showRefResult.getValidRefs());
        if (localRefs.isEmpty() && showRefResult.getInvalidRefs().isEmpty())
            return false;
        for (String invalidRef : showRefResult.getInvalidRefs()) {
            git.updateRef().setRef(invalidRef).delete().call();
            outdatedRefsRemoved = true;
        }
        final Refs remoteRefs;
        try {
            remoteRefs = new Refs(git.lsRemote().setAuthSettings(myRoot.getAuthSettings())
                    .setUseNativeSsh(myPluginConfig.isUseNativeSSH()).call());
        } catch (VcsException e) {
            if (CommandUtil.isCanceledError(e))
                throw e;
            String msg = "Failed to list remote repository refs, outdated local refs will not be cleaned";
            LOG.warn(msg);
            myLogger.warning(msg);
            return false;
        }
        //We remove both outdated local refs (e.g. refs/heads/topic) and outdated remote
        //tracking branches (refs/remote/origin/topic), while git remote origin prune
        //removes only the latter. We need that because in some cases git cannot handle
        //rename of the branch (TW-28735).
        for (Ref localRef : localRefs.list()) {
            Ref correspondingRemoteRef = createCorrespondingRemoteRef(localRef);
            if (remoteRefs.isOutdated(correspondingRemoteRef)) {
                git.updateRef().setRef(localRef.getName()).delete().call();
                outdatedRefsRemoved = true;
            }
        }
        return outdatedRefsRemoved;
    }

    private boolean isRemoteTrackingBranch(@NotNull Ref localRef) {
        return localRef.getName().startsWith("refs/remotes/origin");
    }

    @NotNull
    private Ref createCorrespondingRemoteRef(@NotNull Ref localRef) {
        if (!isRemoteTrackingBranch(localRef))
            return localRef;
        return new RefImpl("refs/heads" + localRef.getName().substring("refs/remotes/origin".length()),
                localRef.getObjectId().name());
    }

    private void configureLFS(@NotNull BaseCommand command) {
        Trinity<String, String, String> lfsAuth = getLfsAuth();
        if (lfsAuth == null)
            return;
        File credentialsHelper = null;
        try {
            ScriptGen scriptGen = myGitFactory.create(new File(".")).getScriptGen();
            final File credHelper = scriptGen.generateCredentialsHelper();
            credentialsHelper = credHelper;
            if (!myPluginConfig.getGitVersion().isLessThan(UpdaterImpl.EMPTY_CRED_HELPER)) {
                //Specify an empty helper if it is supported in order to disable
                //helpers in system-global-local chain. If empty helper is not supported,
                //then the only workaround is to disable helpers manually in config files.
                command.addConfig("credential.helper", "");
            }
            command.addConfig("credential.helper", credHelper.getCanonicalPath());
            CredentialsHelperConfig config = new CredentialsHelperConfig();
            config.addCredentials(lfsAuth.first, lfsAuth.second, lfsAuth.third);
            for (Map.Entry<String, String> e : config.getEnv().entrySet()) {
                command.setEnv(e.getKey(), e.getValue());
            }
            command.addPostAction(new Runnable() {
                @Override
                public void run() {
                    FileUtil.delete(credHelper);
                }
            });
        } catch (Exception e) {
            if (credentialsHelper != null)
                FileUtil.delete(credentialsHelper);
        }
    }

    //returns (url, name, pass) for lfs or null if no authentication is required or
    //root doesn't use http(s)
    @Nullable
    private Trinity<String, String, String> getLfsAuth() {
        try {
            URIish uri = new URIish(myRoot.getRepositoryFetchURL().toString());
            String scheme = uri.getScheme();
            if (myRoot.getAuthSettings().getAuthMethod() == AuthenticationMethod.PASSWORD
                    && ("http".equals(scheme) || "https".equals(scheme))) {
                String lfsUrl = uri.setPass("").setUser("").setPath("").toASCIIString();
                if (lfsUrl.endsWith(".git")) {
                    lfsUrl += "/info/lfs";
                } else {
                    lfsUrl += lfsUrl.endsWith("/") ? ".git/info/lfs" : "/.git/info/lfs";
                }
                return Trinity.create(lfsUrl, myRoot.getAuthSettings().getUserName(),
                        myRoot.getAuthSettings().getPassword());
            }
        } catch (Exception e) {
            LOG.debug("Cannot get lfs auth config", e);
        }
        return null;
    }

    private interface VcsCommand {
        void call() throws VcsException;
    }
}