/** * Copyright (c) 2010, 2013 Darmstadt University of Technology. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Marcel Bruch - initial API and implementation. */ package org.eclipse.recommenders.snipmatch; import static com.google.common.base.Strings.isNullOrEmpty; import static org.eclipse.recommenders.snipmatch.Snippet.FORMAT_VERSION; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.text.MessageFormat; import java.util.Collection; import java.util.List; import org.apache.commons.io.FileUtils; import org.eclipse.core.runtime.CoreException; import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.InitCommand; import org.eclipse.jgit.api.ListBranchCommand; import org.eclipse.jgit.api.ListBranchCommand.ListMode; import org.eclipse.jgit.api.PullCommand; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.InvalidRemoteException; import org.eclipse.jgit.api.errors.TransportException; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.eclipse.jgit.util.FS; import org.eclipse.recommenders.internal.snipmatch.ChainingCredentialsProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.collect.Collections2; public class GitSnippetRepository extends FileSnippetRepository { private static final String FORMAT_PREFIX = "format-"; private static final Logger LOG = LoggerFactory.getLogger(GitSnippetRepository.class); private final File basedir; private final URI fetchUri; private final URI pushUri; private final String pushBranchPrefix; private final File gitFile; private Repository localRepo; public GitSnippetRepository(String id, File basedir, URI uri, URI pushUri, String pushBranchPrefix) { super(id, basedir); this.basedir = basedir; this.fetchUri = uri; this.pushUri = pushUri; this.pushBranchPrefix = pushBranchPrefix; this.gitFile = new File(basedir, ".git"); } @Override public void open() throws IOException { synchronized (this) { boolean updatePossible = false; boolean gitFileExists = gitFile.exists(); if (gitFileExists) { updatePossible = isUpdatePossible(); } try { if (!updatePossible) { if (gitFileExists) { FileUtils.deleteDirectory(gitFile); } initializeSnippetsRepo(); } configureGit(); Git git = fetch(); String checkoutBranch = getCheckoutBranch(git); if (isNullOrEmpty(checkoutBranch)) { throw new GitNoFormatBranchException( MessageFormat.format("Could not locate branch \"{0}\"", FORMAT_VERSION), null); } configureGitBranch(checkoutBranch); pullSnippets(git, checkoutBranch); if (!checkoutBranch.equals(FORMAT_VERSION)) { throw new GitNoCurrentFormatBranchException(checkoutBranch, MessageFormat.format("Could not locate branch \"{0}\", working with older branch \"{1}\".", FORMAT_VERSION, checkoutBranch), null); } } catch (InvalidRemoteException e) { LOG.error("Invalid remote repository.", e); throw createException(updatePossible, MessageFormat .format("Invalid remote repository \"{0}\". Check the repository's URL.", fetchUri), e); } catch (TransportException e) { LOG.error("Transport operation failed.", e); throw createException(updatePossible, "Could not connect to remote repository. Your internet connection may be down.", e); } catch (GitAPIException e) { LOG.error("Exception while update/clone repository.", e); throw createException(updatePossible, "Exception while updating/cloning repository.", e); } catch (CoreException e) { LOG.error("Exception while opening repository.", e); throw createException(updatePossible, "Exception while opening repository.", e); } finally { super.open(); } } } @SuppressWarnings("serial") public static class GitUpdateException extends IOException { public GitUpdateException(String message, Throwable cause) { super(message, cause); } } private IOException createException(boolean gitFileExists, String message, Throwable e) { if (gitFileExists) { return new GitUpdateException(message, e); } else { return new IOException(message, e); } } @SuppressWarnings("serial") public static final class GitNoCurrentFormatBranchException extends IOException { private final String checkoutVersion; public GitNoCurrentFormatBranchException(String checkoutVersion, String message, Throwable cause) { super(message, cause); this.checkoutVersion = checkoutVersion; } public String getCheckoutVersion() { return checkoutVersion; } } @SuppressWarnings("serial") public static final class GitNoFormatBranchException extends IOException { public GitNoFormatBranchException(String message, Throwable cause) { super(message, cause); } } private boolean isUpdatePossible() throws IOException { if (RepositoryCache.FileKey.isGitRepository(gitFile, FS.DETECTED)) { Repository localRepo = new FileRepositoryBuilder().setGitDir(gitFile).build(); for (Ref ref : localRepo.getAllRefs().values()) { if (ref.getObjectId() != null) { return true; } } } return false; } private void initializeSnippetsRepo() throws GitAPIException { InitCommand init = Git.init(); init.setBare(false); init.setDirectory(basedir); init.call(); } private void configureGit() throws IOException { Git git = Git.open(gitFile); StoredConfig config = git.getRepository().getConfig(); config.setString("remote", "origin", "url", getRepositoryLocation()); config.setString("remote", "origin", "fetch", "+refs/heads/*:refs/remotes/origin/*"); config.setString("remote", "origin", "pushUrl", getPushUrl().toString()); config.save(); } private Git fetch() throws GitAPIException, IOException { localRepo = new FileRepositoryBuilder().setGitDir(gitFile).build(); Git git = new Git(localRepo); FetchCommand fetch = git.fetch(); fetch.setCredentialsProvider(getCredentialsProvider(fetchUri)); fetch.call(); return git; } public static CredentialsProvider getCredentialsProvider(URI uri) { URIish urish; try { urish = new URIish(uri.toString()); } catch (URISyntaxException e) { // ignore return CredentialsProvider.getDefault(); } String username = urish.getUser(); String password = urish.getPass(); if (username == null || password == null) { return CredentialsProvider.getDefault(); } UsernamePasswordCredentialsProvider usernamePasswordProvider = new UsernamePasswordCredentialsProvider(username, password); return new ChainingCredentialsProvider(usernamePasswordProvider, CredentialsProvider.getDefault()); } private String getCheckoutBranch(Git git) throws IOException, GitAPIException { ListBranchCommand branchList = git.branchList(); branchList.setListMode(ListMode.REMOTE); List<Ref> branches = branchList.call(); String formatVersion = FORMAT_VERSION.substring(FORMAT_PREFIX.length()); int version = Integer.parseInt(formatVersion); return getCheckoutBranch(branches, version); } private String getCheckoutBranch(List<Ref> branches, int version) { String remoteBranch = "refs/remotes/origin/format-" + version; for (Ref branch : branches) { if (branch.getName().equals(remoteBranch)) { return FORMAT_PREFIX + version; } } if (version > 2) { // 2 == lowest, publicly available version return getCheckoutBranch(branches, version - 1); } return ""; } private void configureGitBranch(String remoteBranch) throws IOException { Git git = Git.open(gitFile); StoredConfig config = git.getRepository().getConfig(); String pushBranch = "HEAD:" + pushBranchPrefix + "/" + remoteBranch; config.setString("remote", "origin", "push", pushBranch); config.setString("branch", remoteBranch, "remote", "origin"); String branch = "refs/heads/" + remoteBranch; config.setString("branch", remoteBranch, "merge", branch); config.save(); } private void pullSnippets(Git git, String checkoutBranch) throws IOException, InvalidRemoteException, TransportException, GitAPIException, CoreException { CheckoutCommand checkout = git.checkout(); checkout.setName(checkoutBranch); checkout.setStartPoint("origin/" + checkoutBranch); checkout.setCreateBranch(!branchExistsLocally(git, "refs/heads/" + checkoutBranch)); checkout.call(); PullCommand pull = git.pull(); pull.setCredentialsProvider(getCredentialsProvider(fetchUri)); pull.call(); } private boolean branchExistsLocally(Git git, String remoteBranch) throws GitAPIException { List<Ref> branches = git.branchList().call(); Collection<String> branchNames = Collections2.transform(branches, new Function<Ref, String>() { @Override public String apply(Ref input) { return input.getName(); } }); return branchNames.contains(remoteBranch); } @Override public String getRepositoryLocation() { return fetchUri.toString(); } @Override public void close() { localRepo.close(); super.close(); }; @Override public boolean delete() { close(); try { FileUtils.deleteDirectory(basedir); return true; } catch (IOException e) { LOG.error("Exception while deleting files on disk.", e); return false; } } public Repository getGitRepo() { return localRepo; } public File getBasedir() { return basedir; } public URI getPushUrl() { return pushUri; } }