/**
* 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;
}
}