org.springframework.cloud.config.server.environment.JGitEnvironmentRepository.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.cloud.config.server.environment.JGitEnvironmentRepository.java

Source

/*
 * Copyright 2013-2015 the original author or authors.
 *
 * 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 org.springframework.cloud.config.server.environment;

import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.jgit.api.CheckoutCommand;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode;
import org.eclipse.jgit.api.FetchCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ListBranchCommand;
import org.eclipse.jgit.api.ListBranchCommand.ListMode;
import org.eclipse.jgit.api.MergeCommand;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.ResetCommand;
import org.eclipse.jgit.api.ResetCommand.ResetType;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.StatusCommand;
import org.eclipse.jgit.api.TransportCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.RefNotFoundException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.JschConfigSessionFactory;
import org.eclipse.jgit.transport.OpenSshConfig.Host;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.TagOpt;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.util.FileUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cloud.config.server.support.PassphraseCredentialsProvider;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.UrlResource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.jcraft.jsch.Session;

import static org.springframework.util.StringUtils.hasText;

/**
 * An {@link EnvironmentRepository} backed by a single git repository.
 *
 * @author Dave Syer
 * @author Roy Clarkson
 * @author Marcos Barbero
 * @author Daniel Lavoie
 * @author Ryan Lynch
 */
public class JGitEnvironmentRepository extends AbstractScmEnvironmentRepository
        implements EnvironmentRepository, SearchPathLocator, InitializingBean {

    private static final String DEFAULT_LABEL = "master";
    private static final String FILE_URI_PREFIX = "file:";

    /**
     * Timeout (in seconds) for obtaining HTTP or SSH connection (if applicable). Default
     * 5 seconds.
     */
    private int timeout = 5;

    private boolean initialized;

    /**
     * Flag to indicate that the repository should be cloned on startup (not on demand).
     * Generally leads to slower startup but faster first query.
     */
    private boolean cloneOnStart = false;

    private JGitEnvironmentRepository.JGitFactory gitFactory = new JGitEnvironmentRepository.JGitFactory();

    private String defaultLabel = DEFAULT_LABEL;

    /**
     * The credentials provider to use to connect to the Git repository.
     */
    private CredentialsProvider gitCredentialsProvider;

    /**
     * Flag to indicate that the repository should force pull. If true discard any local
     * changes and take from remote repository.
     */
    private boolean forcePull;

    public JGitEnvironmentRepository(ConfigurableEnvironment environment) {
        super(environment);
    }

    public boolean isCloneOnStart() {
        return this.cloneOnStart;
    }

    public void setCloneOnStart(boolean cloneOnStart) {
        this.cloneOnStart = cloneOnStart;
    }

    public int getTimeout() {
        return this.timeout;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    public JGitFactory getGitFactory() {
        return this.gitFactory;
    }

    public void setGitFactory(JGitFactory gitFactory) {
        this.gitFactory = gitFactory;
    }

    public String getDefaultLabel() {
        return this.defaultLabel;
    }

    public void setDefaultLabel(String defaultLabel) {
        this.defaultLabel = defaultLabel;
    }

    public boolean isForcePull() {
        return forcePull;
    }

    public void setForcePull(boolean forcePull) {
        this.forcePull = forcePull;
    }

    @Override
    public synchronized Locations getLocations(String application, String profile, String label) {
        if (label == null) {
            label = this.defaultLabel;
        }
        String version = refresh(label);
        return new Locations(application, profile, label, version,
                getSearchLocations(getWorkingDirectory(), application, profile, label));
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.state(getUri() != null, "You need to configure a uri for the git repository");
        initialize();
        if (this.cloneOnStart) {
            initClonedRepository();
        }
    }

    /**
     * Get the working directory ready.
     */
    public String refresh(String label) {
        initialize();
        Git git = null;
        try {
            git = createGitClient();
            if (shouldPull(git)) {
                fetch(git, label);
                //checkout after fetch so we can get any new branches, tags, ect.
                checkout(git, label);
                if (isBranch(git, label)) {
                    //merge results from fetch
                    merge(git, label);
                    if (!isClean(git)) {
                        logger.warn("The local repository is dirty. Resetting it to origin/" + label + ".");
                        resetHard(git, label, "refs/remotes/origin/" + label);
                    }
                }
            } else {
                //nothing to update so just checkout
                checkout(git, label);
            }
            //always return what is currently HEAD as the version
            return git.getRepository().getRef("HEAD").getObjectId().getName();
        } catch (RefNotFoundException e) {
            throw new NoSuchLabelException("No such label: " + label, e);
        } catch (GitAPIException e) {
            throw new IllegalStateException("Cannot clone or checkout repository", e);
        } catch (Exception e) {
            throw new IllegalStateException("Cannot load environment", e);
        } finally {
            try {
                if (git != null) {
                    git.close();
                }
            } catch (Exception e) {
                this.logger.warn("Could not close git repository", e);
            }
        }
    }

    /**
     * Clones the remote repository and then opens a connection to it.
     * @throws GitAPIException
     * @throws IOException
     */
    private void initClonedRepository() throws GitAPIException, IOException {
        if (!getUri().startsWith(FILE_URI_PREFIX)) {
            deleteBaseDirIfExists();
            Git git = cloneToBasedir();
            if (git != null) {
                git.close();
            }
            git = openGitRepository();
            if (git != null) {
                git.close();
            }
        }

    }

    private Ref checkout(Git git, String label) throws GitAPIException {
        CheckoutCommand checkout = git.checkout();
        if (shouldTrack(git, label)) {
            trackBranch(git, checkout, label);
        } else {
            // works for tags and local branches
            checkout.setName(label);
        }
        return checkout.call();
    }

    public /*public for testing*/ boolean shouldPull(Git git) throws GitAPIException {
        boolean shouldPull;
        Status gitStatus = git.status().call();
        boolean isWorkingTreeClean = gitStatus.isClean();
        String originUrl = git.getRepository().getConfig().getString("remote", "origin", "url");

        if (this.forcePull && !isWorkingTreeClean) {
            shouldPull = true;
            logDirty(gitStatus);
        } else {
            shouldPull = isWorkingTreeClean && originUrl != null;
        }
        if (!isWorkingTreeClean && !this.forcePull) {
            this.logger.info("Cannot pull from remote " + originUrl + ", the working tree is not clean.");
        }
        return shouldPull;
    }

    @SuppressWarnings("unchecked")
    private void logDirty(Status status) {
        Set<String> dirties = dirties(status.getAdded(), status.getChanged(), status.getRemoved(),
                status.getMissing(), status.getModified(), status.getConflicting(), status.getUntracked());
        this.logger.warn(String.format("Dirty files found: %s", dirties));
    }

    @SuppressWarnings("unchecked")
    private Set<String> dirties(Set<String>... changes) {
        Set<String> dirties = new HashSet<>();
        for (Set<String> files : changes) {
            dirties.addAll(files);
        }
        return dirties;
    }

    private boolean shouldTrack(Git git, String label) throws GitAPIException {
        return isBranch(git, label) && !isLocalBranch(git, label);
    }

    private FetchResult fetch(Git git, String label) {
        FetchCommand fetch = git.fetch();
        fetch.setRemote("origin");
        fetch.setTagOpt(TagOpt.FETCH_TAGS);

        setTimeout(fetch);
        try {
            setCredentialsProvider(fetch);
            FetchResult result = fetch.call();
            if (result.getTrackingRefUpdates() != null && result.getTrackingRefUpdates().size() > 0) {
                logger.info("Fetched for remote " + label + " and found " + result.getTrackingRefUpdates().size()
                        + " updates");
            }
            return result;
        } catch (Exception ex) {
            String message = "Could not fetch remote for " + label + " remote: "
                    + git.getRepository().getConfig().getString("remote", "origin", "url");
            warn(message, ex);
            return null;
        }
    }

    private MergeResult merge(Git git, String label) {
        try {
            MergeCommand merge = git.merge();
            merge.include(git.getRepository().getRef("origin/" + label));
            MergeResult result = merge.call();
            if (!result.getMergeStatus().isSuccessful()) {
                this.logger.warn("Merged from remote " + label + " with result " + result.getMergeStatus());
            }
            return result;
        } catch (Exception ex) {
            String message = "Could not merge remote for " + label + " remote: "
                    + git.getRepository().getConfig().getString("remote", "origin", "url");
            warn(message, ex);
            return null;
        }
    }

    private Ref resetHard(Git git, String label, String ref) {
        ResetCommand reset = git.reset();
        reset.setRef(ref);
        reset.setMode(ResetType.HARD);
        try {
            Ref resetRef = reset.call();
            if (resetRef != null) {
                this.logger.info("Reset label " + label + " to version " + resetRef.getObjectId());
            }
            return resetRef;
        } catch (Exception ex) {
            String message = "Could not reset to remote for " + label + " (current ref=" + ref + "), remote: "
                    + git.getRepository().getConfig().getString("remote", "origin", "url");
            warn(message, ex);
            return null;
        }
    }

    private Git createGitClient() throws IOException, GitAPIException {
        if (new File(getBasedir(), ".git").exists()) {
            return openGitRepository();
        } else {
            return copyRepository();
        }
    }

    // Synchronize here so that multiple requests don't all try and delete the base dir
    // together (this is a once only operation, so it only holds things up on the first
    // request).
    private synchronized Git copyRepository() throws IOException, GitAPIException {
        deleteBaseDirIfExists();
        getBasedir().mkdirs();
        Assert.state(getBasedir().exists(), "Could not create basedir: " + getBasedir());
        if (getUri().startsWith(FILE_URI_PREFIX)) {
            return copyFromLocalRepository();
        } else {
            return cloneToBasedir();
        }
    }

    private Git openGitRepository() throws IOException {
        Git git = this.gitFactory.getGitByOpen(getWorkingDirectory());
        return git;
    }

    private Git copyFromLocalRepository() throws IOException {
        Git git;
        File remote = new UrlResource(StringUtils.cleanPath(getUri())).getFile();
        Assert.state(remote.isDirectory(), "No directory at " + getUri());
        File gitDir = new File(remote, ".git");
        Assert.state(gitDir.exists(), "No .git at " + getUri());
        Assert.state(gitDir.isDirectory(), "No .git directory at " + getUri());
        git = this.gitFactory.getGitByOpen(remote);
        return git;
    }

    private Git cloneToBasedir() throws GitAPIException {
        CloneCommand clone = this.gitFactory.getCloneCommandByCloneRepository().setURI(getUri())
                .setDirectory(getBasedir());
        setTimeout(clone);
        setCredentialsProvider(clone);
        try {
            return clone.call();
        } catch (GitAPIException e) {
            deleteBaseDirIfExists();
            throw e;
        }
    }

    private void deleteBaseDirIfExists() {
        if (getBasedir().exists()) {
            try {
                FileUtils.delete(getBasedir(), FileUtils.RECURSIVE);
            } catch (IOException e) {
                throw new IllegalStateException("Failed to initialize base directory", e);
            }
        }
    }

    private void initialize() {
        if (!this.initialized) {
            SshSessionFactory.setInstance(new JschConfigSessionFactory() {
                @Override
                protected void configure(Host hc, Session session) {
                    session.setConfig("StrictHostKeyChecking", isStrictHostKeyChecking() ? "yes" : "no");
                }
            });
            this.initialized = true;
        }
    }

    private void setCredentialsProvider(TransportCommand<?, ?> cmd) {
        if (gitCredentialsProvider != null) {
            cmd.setCredentialsProvider(gitCredentialsProvider);
        } else if (hasText(getUsername())) {
            cmd.setCredentialsProvider(new UsernamePasswordCredentialsProvider(getUsername(), getPassword()));
        } else if (hasText(getPassphrase())) {
            cmd.setCredentialsProvider(new PassphraseCredentialsProvider(getPassphrase()));
        }
    }

    private void setTimeout(TransportCommand<?, ?> pull) {
        pull.setTimeout(this.timeout);
    }

    private boolean isClean(Git git) {
        StatusCommand status = git.status();
        try {
            return status.call().isClean();
        } catch (Exception e) {
            String message = "Could not execute status command on local repository. Cause: ("
                    + e.getClass().getSimpleName() + ") " + e.getMessage();
            warn(message, e);
            return false;
        }
    }

    private void trackBranch(Git git, CheckoutCommand checkout, String label) {
        checkout.setCreateBranch(true).setName(label).setUpstreamMode(SetupUpstreamMode.TRACK)
                .setStartPoint("origin/" + label);
    }

    private boolean isBranch(Git git, String label) throws GitAPIException {
        return containsBranch(git, label, ListMode.ALL);
    }

    private boolean isLocalBranch(Git git, String label) throws GitAPIException {
        return containsBranch(git, label, null);
    }

    private boolean containsBranch(Git git, String label, ListMode listMode) throws GitAPIException {
        ListBranchCommand command = git.branchList();
        if (listMode != null) {
            command.setListMode(listMode);
        }
        List<Ref> branches = command.call();
        for (Ref ref : branches) {
            if (ref.getName().endsWith("/" + label)) {
                return true;
            }
        }
        return false;
    }

    protected void warn(String message, Exception ex) {
        logger.warn(message);
        if (logger.isDebugEnabled()) {
            logger.debug("Stacktrace for: " + message, ex);
        }
    }

    /**
     * Wraps the static method calls to {@link org.eclipse.jgit.api.Git} and
     * {@link org.eclipse.jgit.api.CloneCommand} allowing for easier unit testing.
     */
    static class JGitFactory {

        public Git getGitByOpen(File file) throws IOException {
            Git git = Git.open(file);
            return git;
        }

        public CloneCommand getCloneCommandByCloneRepository() {
            CloneCommand command = Git.cloneRepository();
            return command;
        }
    }

    /**
     * @return the gitCredentialsProvider
     */
    public CredentialsProvider getGitCredentialsProvider() {
        return gitCredentialsProvider;
    }

    /**
     * @param gitCredentialsProvider the gitCredentialsProvider to set
     */
    public void setGitCredentialsProvider(CredentialsProvider gitCredentialsProvider) {
        this.gitCredentialsProvider = gitCredentialsProvider;
    }
}