/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* 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:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.api.git;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.che.WorkspaceIdProvider;
import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.UnauthorizedException;
import org.eclipse.che.api.core.model.project.SourceStorage;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.core.util.LineConsumerFactory;
import org.eclipse.che.api.git.exception.GitException;
import org.eclipse.che.api.git.params.CheckoutParams;
import org.eclipse.che.api.git.params.CloneParams;
import org.eclipse.che.api.git.params.FetchParams;
import org.eclipse.che.api.git.params.RemoteAddParams;
import org.eclipse.che.api.git.shared.Branch;
import org.eclipse.che.api.git.shared.GitCheckoutEvent;
import org.eclipse.che.api.project.server.FolderEntry;
import org.eclipse.che.api.project.server.importer.ProjectImporter;
import org.eclipse.che.commons.lang.IoUtil;
import org.eclipse.che.commons.lang.NameGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.eclipse.che.api.core.ErrorCodes.FAILED_CHECKOUT;
import static org.eclipse.che.api.core.ErrorCodes.FAILED_CHECKOUT_WITH_START_POINT;
import static org.eclipse.che.api.git.GitBasicAuthenticationCredentialsProvider.clearCredentials;
import static org.eclipse.che.api.git.GitBasicAuthenticationCredentialsProvider.setCurrentCredentials;
import static org.eclipse.che.api.git.shared.BranchListMode.LIST_ALL;
import static org.eclipse.che.api.git.shared.BranchListMode.LIST_REMOTE;
import static org.eclipse.che.dto.server.DtoFactory.newDto;
/**
* @author Vladyslav Zhukovskii
*/
@Singleton
public class GitProjectImporter implements ProjectImporter {
private static final Logger LOG = LoggerFactory.getLogger(GitProjectImporter.class);
private final GitConnectionFactory gitConnectionFactory;
private final EventService eventService;
@Inject
public GitProjectImporter(GitConnectionFactory gitConnectionFactory,
EventService eventService) {
this.gitConnectionFactory = gitConnectionFactory;
this.eventService = eventService;
}
@Override
public String getId() {
return "git";
}
@Override
public boolean isInternal() {
return false;
}
@Override
public String getDescription() {
return "Import project from hosted GIT repository URL.";
}
/** {@inheritDoc} */
@Override
public ImporterCategory getCategory() {
return ImporterCategory.SOURCE_CONTROL;
}
@Override
public void importSources(FolderEntry baseFolder, SourceStorage storage) throws ForbiddenException,
ConflictException,
UnauthorizedException,
IOException,
ServerException {
importSources(baseFolder, storage, LineConsumerFactory.NULL);
}
@Override
public void importSources(FolderEntry baseFolder,
SourceStorage storage,
LineConsumerFactory consumerFactory) throws ForbiddenException,
ConflictException,
UnauthorizedException,
IOException,
ServerException {
GitConnection git = null;
boolean credentialsHaveBeenSet = false;
try {
// For factory: checkout particular commit after clone
String commitId = null;
// For factory: github pull request feature
String fetch = null;
String branch = null;
String startPoint = null;
// For factory or probably for our projects templates:
// If git repository contains more than one project need clone all repository but after cloning keep just
// sub-project that is specified in parameter "keepDir".
String keepDir = null;
// For factory and for our projects templates:
// Keep all info related to the vcs. In case of Git: ".git" directory and ".gitignore" file.
// Delete vcs info if false.
String branchMerge = null;
boolean keepVcs = true;
boolean recursiveEnabled = false;
boolean convertToTopLevelProject = false;
Map<String, String> parameters = storage.getParameters();
if (parameters != null) {
commitId = parameters.get("commitId");
branch = parameters.get("branch");
startPoint = parameters.get("startPoint");
fetch = parameters.get("fetch");
keepDir = parameters.get("keepDir");
if (parameters.containsKey("keepVcs")) {
keepVcs = Boolean.parseBoolean(parameters.get("keepVcs"));
}
if (parameters.containsKey("recursive")) {
recursiveEnabled = true;
}
//convertToTopLevelProject feature is working only if we don't need any git information
//and when we are working in git sparse checkout mode.
if (!keepVcs && !isNullOrEmpty(keepDir) && parameters.containsKey("convertToTopLevelProject")) {
convertToTopLevelProject = Boolean.parseBoolean(parameters.get("convertToTopLevelProject"));
}
branchMerge = parameters.get("branchMerge");
final String user = storage.getParameters().remove("username");
final String pass = storage.getParameters().remove("password");
if (user != null && pass != null) {
credentialsHaveBeenSet = true;
setCurrentCredentials(user, pass);
}
}
// Get path to local file. Git works with local filesystem only.
final String localPath = baseFolder.getVirtualFile().toIoFile().getAbsolutePath();
final String location = storage.getLocation();
final String projectName = baseFolder.getName();
// Converting steps
// 1. Clone to temporary folder on same device with /projects
// 2. Remove git information
// 3. Move to path requested by user.
// Very important to have initial clone folder on the same drive with /project
// otherwise we will have to replace atomic move with copy-delete operation.
if (convertToTopLevelProject) {
File tempDir = new File(new File(localPath).getParent(), NameGenerator.generate(".che", 6));
git = gitConnectionFactory.getConnection(tempDir, consumerFactory);
} else {
git = gitConnectionFactory.getConnection(localPath, consumerFactory);
}
if (keepDir != null) {
git.cloneWithSparseCheckout(keepDir, location);
if (branch != null) {
git.checkout(CheckoutParams.create(branch));
}
} else {
if (baseFolder.getChildren().size() == 0) {
cloneRepository(git, "origin", location, recursiveEnabled);
if (commitId != null) {
checkoutCommit(git, commitId);
} else if (fetch != null) {
git.getConfig().add("remote.origin.fetch", fetch);
fetch(git, "origin");
if (branch != null) {
checkoutBranch(git, projectName, branch, startPoint);
}
} else if (branch != null) {
checkoutBranch(git, projectName, branch, startPoint);
}
} else {
git.init(false);
addRemote(git, "origin", location);
if (commitId != null) {
fetchBranch(git, "origin", branch == null ? "*" : branch);
checkoutCommit(git, commitId);
} else if (fetch != null) {
git.getConfig().add("remote.origin.fetch", fetch);
fetch(git, "origin");
if (branch != null) {
checkoutBranch(git, projectName, branch, startPoint);
}
} else {
fetchBranch(git, "origin", branch == null ? "*" : branch);
List<Branch> branchList = git.branchList(LIST_REMOTE);
if (!branchList.isEmpty()) {
checkoutBranch(git, projectName, branch == null ? "master" : branch, startPoint);
}
}
}
if (branchMerge != null) {
git.getConfig().set("branch." + (branch == null ? "master" : branch) + ".merge", branchMerge);
}
}
if (!keepVcs) {
cleanGit(git.getWorkingDir());
}
if (convertToTopLevelProject) {
Files.move(new File(git.getWorkingDir(), keepDir).toPath(),
new File(localPath).toPath(),
StandardCopyOption.ATOMIC_MOVE);
IoUtil.deleteRecursive(git.getWorkingDir());
}
} catch (URISyntaxException e) {
throw new ServerException(
"Your project cannot be imported. The issue is either from git configuration, a malformed URL, " +
"or file system corruption. Please contact support for assistance.",
e);
} finally {
if (git != null) {
git.close();
}
if (credentialsHaveBeenSet) {
clearCredentials();
}
}
}
private void cloneRepository(GitConnection git, String remoteName, String url, boolean recursiveEnabled)
throws ServerException, UnauthorizedException, URISyntaxException {
final CloneParams params = CloneParams.create(url).withRemoteName(remoteName).withRecursive(recursiveEnabled);
git.clone(params);
}
private void addRemote(GitConnection git, String name, String url) throws GitException {
git.remoteAdd(RemoteAddParams.create(name, url));
}
private void fetch(GitConnection git, String remote) throws UnauthorizedException, GitException {
final FetchParams params = FetchParams.create(remote);
git.fetch(params);
}
private void fetchBranch(GitConnection gitConnection, String remote, String branch)
throws UnauthorizedException, GitException {
final List<String> refSpecs = Collections.singletonList(String.format("refs/heads/%1$s:refs/remotes/origin/%1$s", branch));
try {
fetchRefSpecs(gitConnection, remote, refSpecs);
} catch (GitException e) {
LOG.warn("Git exception on branch fetch", e);
throw new GitException(
String.format("Unable to fetch remote branch %s. Make sure it exists and can be accessed.", branch),
e);
}
}
private void fetchRefSpecs(GitConnection git, String remote, List<String> refSpecs)
throws UnauthorizedException, GitException {
final FetchParams params = FetchParams.create(remote).withRefSpec(refSpecs);
git.fetch(params);
}
private void checkoutCommit(GitConnection git, String commit) throws GitException {
final CheckoutParams params = CheckoutParams.create("temp")
.withCreateNew(true)
.withStartPoint(commit);
try {
git.checkout(params);
} catch (GitException e) {
LOG.warn("Git exception on commit checkout", e);
throw new GitException(
String.format("Unable to checkout commit %s. Make sure it exists and can be accessed.", commit), e);
}
}
private void checkoutBranch(GitConnection git,
String projectName,
String branchName,
String startPoint) throws GitException {
final CheckoutParams params = CheckoutParams.create(branchName);
final boolean branchExist = git.branchList(LIST_ALL)
.stream()
.anyMatch(branch -> branch.getDisplayName().equals("origin/" + branchName));
final GitCheckoutEvent checkout = newDto(GitCheckoutEvent.class).withWorkspaceId(WorkspaceIdProvider.getWorkspaceId())
.withProjectName(projectName);
if (startPoint != null) {
if (branchExist) {
git.checkout(params);
eventService.publish(checkout.withCheckoutOnly(true)
.withBranchRef(getRemoteBranch(git, branchName)));
} else {
checkoutAndRethrow(git, params.withCreateNew(true).withStartPoint(startPoint).withNoTrack(true),
FAILED_CHECKOUT_WITH_START_POINT);
eventService.publish(checkout.withCheckoutOnly(false));
}
} else {
checkoutAndRethrow(git, params, FAILED_CHECKOUT);
eventService.publish(checkout.withCheckoutOnly(true)
.withBranchRef(getRemoteBranch(git, branchName)));
}
}
private void checkoutAndRethrow(GitConnection git, CheckoutParams params, int errorCode) throws GitException {
try {
git.checkout(params);
} catch (GitException ex) {
throw new GitException(ex.getMessage(), errorCode);
}
}
private void cleanGit(File project) {
IoUtil.deleteRecursive(new File(project, ".git"));
new File(project, ".gitignore").delete();
}
private String getRemoteBranch(GitConnection git, String branchName) throws GitException {
final List<Branch> remotes = git.branchList(LIST_REMOTE);
final Optional<Branch> first = remotes.stream()
.filter(br -> branchName.equals(br.getName().substring(br.getName().lastIndexOf("/") + 1)))
.findFirst();
if (!first.isPresent()) {
throw new GitException("Failed to get remote branch name", FAILED_CHECKOUT);
}
return first.get().getName();
}
}