Java tutorial
/******************************************************************************* * Copyright (c) 2012-2016 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 * SAP - implementation *******************************************************************************/ package org.eclipse.che.git.impl.jgit; import com.google.common.io.Files; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; import com.jcraft.jsch.Session; import org.apache.commons.io.filefilter.DirectoryFileFilter; import org.apache.commons.io.filefilter.IOFileFilter; import org.apache.commons.io.filefilter.TrueFileFilter; import org.eclipse.che.api.core.ErrorCodes; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.UnauthorizedException; import org.eclipse.che.api.core.util.LineConsumerFactory; import org.eclipse.che.api.git.Config; import org.eclipse.che.api.git.CredentialsLoader; import org.eclipse.che.api.git.DiffPage; import org.eclipse.che.api.git.GitConnection; import org.eclipse.che.api.git.GitException; import org.eclipse.che.api.git.GitUrlUtils; import org.eclipse.che.api.git.GitUserResolver; import org.eclipse.che.api.git.LogPage; import org.eclipse.che.api.git.UserCredential; import org.eclipse.che.api.git.shared.AddRequest; import org.eclipse.che.api.git.shared.Branch; import org.eclipse.che.api.git.shared.BranchCreateRequest; import org.eclipse.che.api.git.shared.BranchDeleteRequest; import org.eclipse.che.api.git.shared.BranchListRequest; import org.eclipse.che.api.git.shared.CheckoutRequest; import org.eclipse.che.api.git.shared.CloneRequest; import org.eclipse.che.api.git.shared.CommitRequest; import org.eclipse.che.api.git.shared.DiffRequest; import org.eclipse.che.api.git.shared.FetchRequest; import org.eclipse.che.api.git.shared.GitUser; import org.eclipse.che.api.git.shared.InitRequest; import org.eclipse.che.api.git.shared.LogRequest; import org.eclipse.che.api.git.shared.LsFilesRequest; import org.eclipse.che.api.git.shared.LsRemoteRequest; import org.eclipse.che.api.git.shared.MergeRequest; import org.eclipse.che.api.git.shared.MergeResult; import org.eclipse.che.api.git.shared.MoveRequest; import org.eclipse.che.api.git.shared.PullRequest; import org.eclipse.che.api.git.shared.PullResponse; import org.eclipse.che.api.git.shared.PushRequest; import org.eclipse.che.api.git.shared.PushResponse; import org.eclipse.che.api.git.shared.RebaseRequest; import org.eclipse.che.api.git.shared.RebaseResponse; import org.eclipse.che.api.git.shared.RebaseResponse.RebaseStatus; import org.eclipse.che.api.git.shared.Remote; import org.eclipse.che.api.git.shared.RemoteAddRequest; import org.eclipse.che.api.git.shared.RemoteListRequest; import org.eclipse.che.api.git.shared.RemoteReference; import org.eclipse.che.api.git.shared.RemoteUpdateRequest; import org.eclipse.che.api.git.shared.ResetRequest; import org.eclipse.che.api.git.shared.Revision; import org.eclipse.che.api.git.shared.RmRequest; import org.eclipse.che.api.git.shared.ShowFileContentRequest; import org.eclipse.che.api.git.shared.ShowFileContentResponse; import org.eclipse.che.api.git.shared.Status; import org.eclipse.che.api.git.shared.StatusFormat; import org.eclipse.che.api.git.shared.Tag; import org.eclipse.che.api.git.shared.TagCreateRequest; import org.eclipse.che.api.git.shared.TagDeleteRequest; import org.eclipse.che.api.git.shared.TagListRequest; import org.eclipse.che.plugin.ssh.key.script.SshKeyProvider; import org.eclipse.jgit.api.AddCommand; import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.CommitCommand; import org.eclipse.jgit.api.CreateBranchCommand; 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.LogCommand; import org.eclipse.jgit.api.LsRemoteCommand; import org.eclipse.jgit.api.PushCommand; import org.eclipse.jgit.api.RebaseCommand; import org.eclipse.jgit.api.RebaseResult; import org.eclipse.jgit.api.ResetCommand; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.RmCommand; import org.eclipse.jgit.api.TagCommand; import org.eclipse.jgit.api.TransportCommand; import org.eclipse.jgit.api.TransportConfigCallback; import org.eclipse.jgit.api.errors.CheckoutConflictException; import org.eclipse.jgit.api.errors.DetachedHeadException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.TransportException; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.merge.ResolveMerger; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevTag; import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.FetchResult; import org.eclipse.jgit.transport.JschConfigSessionFactory; import org.eclipse.jgit.transport.OpenSshConfig; import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteConfig; import org.eclipse.jgit.transport.RemoteRefUpdate; import org.eclipse.jgit.transport.TrackingRefUpdate; import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.SshTransport; import org.eclipse.jgit.transport.Transport; import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.PathFilter; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.File; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; import static com.google.common.base.Strings.isNullOrEmpty; import static java.lang.System.lineSeparator; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import static org.eclipse.che.dto.server.DtoFactory.newDto; /** * @author Andrey Parfonov * @author Igor Vinokur */ class JGitConnection implements GitConnection { private static final String REBASE_OPERATION_SKIP = "SKIP"; private static final String REBASE_OPERATION_CONTINUE = "CONTINUE"; private static final String REBASE_OPERATION_ABORT = "ABORT"; private static final String ADD_ALL_OPTION = "all"; // Push Response Constants private static final String BRANCH_REFSPEC_SEPERATOR = " -> "; private static final String REFSPEC_COLON = ":"; private static final String KEY_COMMIT_MESSAGE = "Message"; private static final String KEY_RESULT = "Result"; private static final String KEY_REMOTENAME = "RemoteName"; private static final String KEY_LOCALNAME = "LocalName"; private static final String INFO_PUSH_ATTEMPT_IGNORED_UP_TO_DATE = "Could not push because the repository is up-to-date."; private static final String ERROR_PUSH_ATTEMPT_FAILED_WITHMSG = "Could not push refspec {0} to the branch {1}. Try to merge " + "the remote changes using pull, and then push again. \n Error " + "Status: {2}, Error message {3}. \n"; private static final String ERROR_PUSH_ATTEMPT_FAILED_WITHOUTMSG = "Could not push refspec {0} to the branch {1}. Try to merge " + "the remote changes using pull, and then push again. \n " + "Error Status: {2}. \n"; private static final String ERROR_UPDATE_REMOTE_NAME_MISSING = "Update operation failed, remote name is required."; private static final String ERROR_ADD_REMOTE_NAME_MISSING = "Add operation failed, remote name is required."; private static final String ERROR_REMOTE_NAME_ALREADY_EXISTS = "Add Remote operation failed, remote name %s already exists."; private static final String ERROR_REMOTE_URL_MISSING = "Add Remote operation failed, Remote url required."; private static final String ERROR_PULL_MERGING = "Could not pull because the repository state is 'MERGING'."; private static final String ERROR_PULL_HEAD_DETACHED = "Could not pull because HEAD is detached."; private static final String ERROR_PULL_REF_MISSING = "Could not pull because remote ref is missing for branch %s."; private static final String ERROR_PULL_AUTO_MERGE_FAILED = "Automatic merge failed; fix conflicts and then commit the result."; private static final String ERROR_PULL_MERGE_CONFLICT_IN_FILES = "Could not pull because a merge conflict is detected in the files:"; private static final String ERROR_PULL_COMMIT_BEFORE_MERGE = "Could not pull. Commit your changes before merging."; private static final String ERROR_TAG_DELETE = "Could not delete the tag %1$s. An error occurred: %2$s."; private static final String ERROR_INIT_FOLDER_MISSING = "The working folder %s does not exist."; private static final String ERROR_CHECKOUT_CONFLICT = "Checkout operation failed, the following files would be " + "overwritten by merge:"; private static final String ERROR_REMOVING_INVALID_URL = "remoteUpdate: Ignore this error. Cannot remove invalid URL."; private static final String ERROR_BRANCH_NAME_EXISTS = "A branch named '%s' already exists."; private static final String ERROR_UNSUPPORTED_LIST_MODE = "Unsupported list mode '%s'. Must be either 'a' or 'r'."; private static final String ERROR_NO_REMOTE_REPOSITORY = "No remote repository specified. Please, specify either a URL or a " + "remote name from which new revisions should be fetched in request."; private static final String ERROR_AUTHENTICATION_REQUIRED = "Authentication is required but no CredentialsProvider has " + "been registered"; private static final String ERROR_AUTHENTICATION_FAILED = "fatal: Authentication failed for '%s/'" + lineSeparator(); private static final String ERROR_NO_HEAD_EXISTS = "No HEAD exists and no explicit starting revision was specified"; private static final String MESSAGE_COMMIT_NOT_POSSIBLE = "Commit is not possible because repository state is '%s'"; private static final String MESSAGE_AMEND_NOT_POSSIBLE = "Amend is not possible because repository state is '%s'"; private static final Logger LOG = LoggerFactory.getLogger(JGitConnection.class); private Git git; private JGitConfigImpl config; private final CredentialsLoader credentialsLoader; private final SshKeyProvider sshKeyProvider; private final GitUserResolver userResolver; private final Repository repository; @Inject JGitConnection(Repository repository, CredentialsLoader credentialsLoader, SshKeyProvider sshKeyProvider, GitUserResolver userResolver) { this.repository = repository; this.credentialsLoader = credentialsLoader; this.sshKeyProvider = sshKeyProvider; this.userResolver = userResolver; } @Override public void add(AddRequest request) throws GitException { add(request, request.isUpdate()); // "all" option, when update is false, should run git add with both update true and update false if ((!request.isUpdate()) && request.getAttributes().containsKey(ADD_ALL_OPTION)) { add(request, true); } } /* * Perform the "add" according to the add request. isUpdate is always used * as the value for the "update" parameter instead of the value in the * AddRequest. */ private void add(AddRequest request, boolean isUpdate) throws GitException { AddCommand addCommand = getGit().add().setUpdate(isUpdate); List<String> filePatterns = request.getFilepattern(); if (filePatterns.isEmpty()) { filePatterns = AddRequest.DEFAULT_PATTERN; } filePatterns.forEach(addCommand::addFilepattern); try { addCommand.call(); } catch (GitAPIException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public void checkout(CheckoutRequest request) throws GitException { CheckoutCommand checkoutCommand = getGit().checkout(); String startPoint = request.getStartPoint(); String name = request.getName(); String trackBranch = request.getTrackBranch(); // checkout files? List<String> files = request.getFiles(); boolean shouldCheckoutToFile = name != null && new File(getWorkingDir(), name).exists(); if (shouldCheckoutToFile || !files.isEmpty()) { if (shouldCheckoutToFile) { checkoutCommand.addPath(request.getName()); } else { files.forEach(checkoutCommand::addPath); } } else { // checkout branch if (startPoint != null && trackBranch != null) { throw new GitException("Start point and track branch can not be used together."); } if (request.isCreateNew() && name == null) { throw new GitException("Branch name must be set when createNew equals to true."); } if (startPoint != null) { checkoutCommand.setStartPoint(startPoint); } if (request.isCreateNew()) { checkoutCommand.setCreateBranch(true); checkoutCommand.setName(name); } else if (name != null) { checkoutCommand.setName(name); List<String> localBranches = branchList( newDto(BranchListRequest.class).withListMode(BranchListRequest.LIST_LOCAL)).stream() .map(Branch::getDisplayName).collect(Collectors.toList()); if (!localBranches.contains(name)) { Optional<Branch> remoteBranch = branchList( newDto(BranchListRequest.class).withListMode(BranchListRequest.LIST_REMOTE)).stream() .filter(branch -> branch.getName().contains(name)).findFirst(); if (remoteBranch.isPresent()) { checkoutCommand.setCreateBranch(true); checkoutCommand.setStartPoint(remoteBranch.get().getName()); } } } if (trackBranch != null) { if (name == null) { checkoutCommand.setName(cleanRemoteName(trackBranch)); } checkoutCommand.setCreateBranch(true); checkoutCommand.setStartPoint(trackBranch); } checkoutCommand.setUpstreamMode(SetupUpstreamMode.SET_UPSTREAM); } try { checkoutCommand.call(); } catch (GitAPIException exception) { if (exception.getMessage().endsWith("already exists")) { throw new GitException(String.format(ERROR_BRANCH_NAME_EXISTS, name != null ? name : cleanRemoteName(trackBranch))); } throw new GitException(exception.getMessage(), exception); } } @Override public Branch branchCreate(BranchCreateRequest request) throws GitException { CreateBranchCommand createBranchCommand = getGit().branchCreate().setName(request.getName()); String start = request.getStartPoint(); if (start != null) { createBranchCommand.setStartPoint(start); } try { Ref brRef = createBranchCommand.call(); String refName = brRef.getName(); String displayName = Repository.shortenRefName(refName); return newDto(Branch.class).withName(refName).withDisplayName(displayName).withActive(false) .withRemote(false); } catch (GitAPIException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public void branchDelete(BranchDeleteRequest request) throws GitException { try { getGit().branchDelete().setBranchNames(request.getName()).setForce(request.isForce()).call(); } catch (GitAPIException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public void branchRename(String oldName, String newName) throws GitException { try { getGit().branchRename().setOldName(oldName).setNewName(newName).call(); } catch (GitAPIException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public List<Branch> branchList(BranchListRequest request) throws GitException { String listMode = request.getListMode(); if (listMode != null && !BranchListRequest.LIST_ALL.equals(listMode) && !BranchListRequest.LIST_REMOTE.equals(listMode)) { throw new IllegalArgumentException(String.format(ERROR_UNSUPPORTED_LIST_MODE, listMode)); } ListBranchCommand listBranchCommand = getGit().branchList(); if (BranchListRequest.LIST_ALL.equals(listMode)) { listBranchCommand.setListMode(ListMode.ALL); } else if (BranchListRequest.LIST_REMOTE.equals(listMode)) { listBranchCommand.setListMode(ListMode.REMOTE); } List<Ref> refs; String currentRef; try { refs = listBranchCommand.call(); String headBranch = getRepository().getBranch(); Optional<Ref> currentTag = getGit().tagList().call().stream() .filter(tag -> tag.getObjectId().getName().equals(headBranch)).findFirst(); if (currentTag.isPresent()) { currentRef = currentTag.get().getName(); } else { currentRef = "refs/heads/" + headBranch; } } catch (GitAPIException | IOException exception) { throw new GitException(exception.getMessage(), exception); } List<Branch> branches = new ArrayList<>(); for (Ref ref : refs) { String refName = ref.getName(); boolean isCommitOrTag = Constants.HEAD.equals(refName); String branchName = isCommitOrTag ? currentRef : refName; String branchDisplayName; if (isCommitOrTag) { branchDisplayName = "(detached from " + Repository.shortenRefName(currentRef) + ")"; } else { branchDisplayName = Repository.shortenRefName(refName); } Branch branch = newDto(Branch.class).withName(branchName) .withActive(isCommitOrTag || refName.equals(currentRef)).withDisplayName(branchDisplayName) .withRemote(refName.startsWith("refs/remotes")); branches.add(branch); } return branches; } public void clone(CloneRequest request) throws GitException, UnauthorizedException { String remoteUri; boolean removeIfFailed = false; try { if (request.getRemoteName() == null) { request.setRemoteName(Constants.DEFAULT_REMOTE_NAME); } if (request.getWorkingDir() == null) { request.setWorkingDir(repository.getWorkTree().getCanonicalPath()); } // If clone fails and the .git folder didn't exist we want to remove it. // We have to do this here because the clone command doesn't revert its own changes in case of failure. removeIfFailed = !repository.getDirectory().exists(); remoteUri = request.getRemoteUri(); CloneCommand cloneCommand = Git.cloneRepository().setDirectory(new File(request.getWorkingDir())) .setRemote(request.getRemoteName()).setURI(remoteUri); if (request.getBranchesToFetch().isEmpty()) { cloneCommand.setCloneAllBranches(true); } else { cloneCommand.setBranchesToClone(request.getBranchesToFetch()); } executeRemoteCommand(remoteUri, cloneCommand); StoredConfig repositoryConfig = getRepository().getConfig(); GitUser gitUser = getUser(); if (gitUser != null) { repositoryConfig.setString(ConfigConstants.CONFIG_USER_SECTION, null, ConfigConstants.CONFIG_KEY_NAME, gitUser.getName()); repositoryConfig.setString(ConfigConstants.CONFIG_USER_SECTION, null, ConfigConstants.CONFIG_KEY_EMAIL, gitUser.getEmail()); } repositoryConfig.save(); } catch (IOException | GitAPIException exception) { // Delete .git directory in case it was created if (removeIfFailed) { deleteRepositoryFolder(); } throw new GitException(exception.getMessage(), exception); } } @Override public Revision commit(CommitRequest request) throws GitException { try { String message = request.getMessage(); GitUser committer = getUser(); if (message == null) { throw new GitException("Message wasn't set"); } if (committer == null) { throw new GitException("Committer can't be null"); } String committerName = committer.getName(); String committerEmail = committer.getEmail(); if (committerName == null || committerEmail == null) { throw new GitException("Git user name and (or) email wasn't set", ErrorCodes.NO_COMMITTER_NAME_OR_EMAIL_DEFINED); } if (!repository.getRepositoryState().canCommit()) { Revision rev = newDto(Revision.class); rev.setMessage(String.format(MESSAGE_COMMIT_NOT_POSSIBLE, repository.getRepositoryState().getDescription())); return rev; } if (request.isAmend() && !repository.getRepositoryState().canAmend()) { Revision rev = newDto(Revision.class); rev.setMessage(String.format(MESSAGE_AMEND_NOT_POSSIBLE, repository.getRepositoryState().getDescription())); return rev; } CommitCommand commitCommand = getGit().commit().setCommitter(committerName, committerEmail) .setAuthor(committerName, committerEmail).setMessage(message).setAll(request.isAll()) .setAmend(request.isAmend()); // Check if repository is configured with Gerrit Support String gerritSupportConfigValue = repository.getConfig().getString( ConfigConstants.CONFIG_GERRIT_SECTION, null, ConfigConstants.CONFIG_KEY_CREATECHANGEID); boolean isGerritSupportConfigured = gerritSupportConfigValue != null ? Boolean.valueOf(gerritSupportConfigValue) : false; commitCommand.setInsertChangeId(isGerritSupportConfigured); RevCommit result = commitCommand.call(); GitUser gitUser = newDto(GitUser.class).withName(committerName).withEmail(committerEmail); return newDto(Revision.class).withBranch(getCurrentBranch()).withId(result.getId().getName()) .withMessage(result.getFullMessage()) .withCommitTime(MILLISECONDS.convert(result.getCommitTime(), SECONDS)).withCommitter(gitUser); } catch (GitAPIException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public DiffPage diff(DiffRequest request) throws GitException { return new JGitDiffPage(request, repository); } @Override public boolean isInsideWorkTree() throws GitException { return RepositoryCache.FileKey.isGitRepository(getRepository().getDirectory(), FS.DETECTED); } @Override public ShowFileContentResponse showFileContent(ShowFileContentRequest request) throws GitException { String content; ObjectId revision; try { revision = getRepository().resolve(request.getVersion()); try (RevWalk revWalk = new RevWalk(getRepository())) { RevCommit revCommit = revWalk.parseCommit(revision); RevTree tree = revCommit.getTree(); try (TreeWalk treeWalk = new TreeWalk(getRepository())) { treeWalk.addTree(tree); treeWalk.setRecursive(true); treeWalk.setFilter(PathFilter.create(request.getFile())); if (!treeWalk.next()) { throw new GitException("fatal: Path '" + request.getFile() + "' does not exist in '" + request.getVersion() + "'" + lineSeparator()); } ObjectId objectId = treeWalk.getObjectId(0); ObjectLoader loader = repository.open(objectId); content = new String(loader.getBytes()); } } } catch (IOException exception) { throw new GitException(exception.getMessage()); } return newDto(ShowFileContentResponse.class).withContent(content); } @Override public void fetch(FetchRequest request) throws GitException, UnauthorizedException { String remoteName = request.getRemote(); String remoteUri; try { List<RefSpec> fetchRefSpecs; List<String> refSpec = request.getRefSpec(); if (!refSpec.isEmpty()) { fetchRefSpecs = new ArrayList<>(refSpec.size()); for (String refSpecItem : refSpec) { RefSpec fetchRefSpec = (refSpecItem.indexOf(':') < 0) // ? new RefSpec(Constants.R_HEADS + refSpecItem + ":") // : new RefSpec(refSpecItem); fetchRefSpecs.add(fetchRefSpec); } } else { fetchRefSpecs = Collections.emptyList(); } FetchCommand fetchCommand = getGit().fetch(); // If this an unknown remote with no refspecs given, put HEAD // (otherwise JGit fails) if (remoteName != null && refSpec.isEmpty()) { boolean found = false; List<Remote> configRemotes = remoteList(newDto(RemoteListRequest.class)); for (Remote configRemote : configRemotes) { if (remoteName.equals(configRemote.getName())) { found = true; break; } } if (!found) { fetchRefSpecs = Collections .singletonList(new RefSpec(Constants.HEAD + ":" + Constants.FETCH_HEAD)); } } if (remoteName == null) { remoteName = Constants.DEFAULT_REMOTE_NAME; } fetchCommand.setRemote(remoteName); remoteUri = getRepository().getConfig().getString(ConfigConstants.CONFIG_REMOTE_SECTION, remoteName, ConfigConstants.CONFIG_KEY_URL); fetchCommand.setRefSpecs(fetchRefSpecs); int timeout = request.getTimeout(); if (timeout > 0) { fetchCommand.setTimeout(timeout); } fetchCommand.setRemoveDeletedRefs(request.isRemoveDeletedRefs()); executeRemoteCommand(remoteUri, fetchCommand); } catch (GitException | GitAPIException exception) { String errorMessage; if (exception.getMessage().contains("Invalid remote: ")) { errorMessage = ERROR_NO_REMOTE_REPOSITORY; } else if ("Nothing to fetch.".equals(exception.getMessage())) { return; } else { errorMessage = exception.getMessage(); } throw new GitException(errorMessage, exception); } } @Override public void init(InitRequest request) throws GitException { File workDir = repository.getWorkTree(); if (!workDir.exists()) { throw new GitException(String.format(ERROR_INIT_FOLDER_MISSING, workDir)); } // If create fails and the .git folder didn't exist we want to remove it. // We have to do this here because the create command doesn't revert its own changes in case of failure. boolean removeIfFailed = !repository.getDirectory().exists(); try { repository.create(request.isBare()); } catch (IOException exception) { if (removeIfFailed) { deleteRepositoryFolder(); } throw new GitException(exception.getMessage(), exception); } } @Override public LogPage log(LogRequest request) throws GitException { LogCommand logCommand = getGit().log(); try { setRevisionRange(logCommand, request); request.getFileFilter().forEach(logCommand::addPath); Iterator<RevCommit> revIterator = logCommand.call().iterator(); List<Revision> commits = new ArrayList<>(); while (revIterator.hasNext()) { RevCommit commit = revIterator.next(); PersonIdent committerIdentity = commit.getCommitterIdent(); GitUser gitUser = newDto(GitUser.class).withName(committerIdentity.getName()) .withEmail(committerIdentity.getEmailAddress()); Revision revision = newDto(Revision.class).withId(commit.getId().getName()) .withMessage(commit.getFullMessage()) .withCommitTime(MILLISECONDS.convert(commit.getCommitTime(), SECONDS)) .withCommitter(gitUser); commits.add(revision); } return new LogPage(commits); } catch (GitAPIException | IOException exception) { String errorMessage = exception.getMessage(); if (ERROR_NO_HEAD_EXISTS.equals(errorMessage)) { throw new GitException(errorMessage, ErrorCodes.INIT_COMMIT_WAS_NOT_PERFORMED); } throw new GitException(errorMessage, exception); } } private void setRevisionRange(LogCommand logCommand, LogRequest request) throws IOException { if (request != null) { String revisionRangeSince = request.getRevisionRangeSince(); String revisionRangeUntil = request.getRevisionRangeUntil(); if (revisionRangeSince != null && revisionRangeUntil != null) { ObjectId since = repository.resolve(revisionRangeSince); ObjectId until = repository.resolve(revisionRangeUntil); logCommand.addRange(since, until); } } } @Override public List<GitUser> getCommiters() throws GitException { List<GitUser> gitUsers = new ArrayList<>(); try { LogCommand logCommand = getGit().log(); for (RevCommit commit : logCommand.call()) { PersonIdent committerIdentity = commit.getCommitterIdent(); GitUser gitUser = newDto(GitUser.class).withName(committerIdentity.getName()) .withEmail(committerIdentity.getEmailAddress()); if (!gitUsers.contains(gitUser)) { gitUsers.add(gitUser); } } } catch (GitAPIException exception) { throw new GitException(exception.getMessage(), exception); } return gitUsers; } @Override public MergeResult merge(MergeRequest request) throws GitException { org.eclipse.jgit.api.MergeResult jGitMergeResult; MergeResult.MergeStatus status; try { Ref ref = repository.findRef(request.getCommit()); if (ref == null) { throw new IllegalArgumentException("Invalid reference to commit for merge " + request.getCommit()); } // Shorten local branch names by removing '/refs/heads/' from the beginning String name = ref.getName(); if (name.startsWith(Constants.R_HEADS)) { name = name.substring(Constants.R_HEADS.length()); } jGitMergeResult = getGit().merge().include(name, ref.getObjectId()).call(); } catch (CheckoutConflictException exception) { jGitMergeResult = new org.eclipse.jgit.api.MergeResult(exception.getConflictingPaths()); } catch (IOException | GitAPIException exception) { throw new GitException(exception.getMessage(), exception); } switch (jGitMergeResult.getMergeStatus()) { case ALREADY_UP_TO_DATE: status = MergeResult.MergeStatus.ALREADY_UP_TO_DATE; break; case CONFLICTING: status = MergeResult.MergeStatus.CONFLICTING; break; case FAILED: status = MergeResult.MergeStatus.FAILED; break; case FAST_FORWARD: status = MergeResult.MergeStatus.FAST_FORWARD; break; case MERGED: status = MergeResult.MergeStatus.MERGED; break; case NOT_SUPPORTED: status = MergeResult.MergeStatus.NOT_SUPPORTED; break; case CHECKOUT_CONFLICT: status = MergeResult.MergeStatus.CONFLICTING; break; default: throw new IllegalStateException("Unknown merge status " + jGitMergeResult.getMergeStatus()); } ObjectId[] jGitMergedCommits = jGitMergeResult.getMergedCommits(); List<String> mergedCommits = new ArrayList<>(); if (jGitMergedCommits != null) { for (ObjectId commit : jGitMergedCommits) { mergedCommits.add(commit.getName()); } } List<String> conflicts; if (org.eclipse.jgit.api.MergeResult.MergeStatus.CHECKOUT_CONFLICT .equals(jGitMergeResult.getMergeStatus())) { conflicts = jGitMergeResult.getCheckoutConflicts(); } else { Map<String, int[][]> jGitConflicts = jGitMergeResult.getConflicts(); conflicts = jGitConflicts != null ? new ArrayList<>(jGitConflicts.keySet()) : Collections.emptyList(); } Map<String, ResolveMerger.MergeFailureReason> jGitFailing = jGitMergeResult.getFailingPaths(); ObjectId newHead = jGitMergeResult.getNewHead(); return newDto(MergeResult.class) .withFailed(jGitFailing != null ? new ArrayList<>(jGitFailing.keySet()) : Collections.emptyList()) .withNewHead(newHead != null ? newHead.getName() : null).withMergeStatus(status) .withConflicts(conflicts).withMergedCommits(mergedCommits); } @Override public RebaseResponse rebase(RebaseRequest request) throws GitException { RebaseResult result; RebaseStatus status; List<String> failed; List<String> conflicts; try { RebaseCommand rebaseCommand = getGit().rebase(); setRebaseOperation(rebaseCommand, request); String branch = request.getBranch(); if (branch != null && !branch.isEmpty()) { rebaseCommand.setUpstream(branch); } result = rebaseCommand.call(); } catch (GitAPIException exception) { throw new GitException(exception.getMessage(), exception); } switch (result.getStatus()) { case ABORTED: status = RebaseStatus.ABORTED; break; case CONFLICTS: status = RebaseStatus.CONFLICTING; break; case UP_TO_DATE: status = RebaseStatus.ALREADY_UP_TO_DATE; break; case FAST_FORWARD: status = RebaseStatus.FAST_FORWARD; break; case NOTHING_TO_COMMIT: status = RebaseStatus.NOTHING_TO_COMMIT; break; case OK: status = RebaseStatus.OK; break; case STOPPED: status = RebaseStatus.STOPPED; break; case UNCOMMITTED_CHANGES: status = RebaseStatus.UNCOMMITTED_CHANGES; break; case EDIT: status = RebaseStatus.EDITED; break; case INTERACTIVE_PREPARED: status = RebaseStatus.INTERACTIVE_PREPARED; break; case STASH_APPLY_CONFLICTS: status = RebaseStatus.STASH_APPLY_CONFLICTS; break; default: status = RebaseStatus.FAILED; } conflicts = result.getConflicts() != null ? result.getConflicts() : Collections.emptyList(); failed = result.getFailingPaths() != null ? new ArrayList<>(result.getFailingPaths().keySet()) : Collections.emptyList(); return newDto(RebaseResponse.class).withStatus(status).withConflicts(conflicts).withFailed(failed); } private void setRebaseOperation(RebaseCommand rebaseCommand, RebaseRequest request) { RebaseCommand.Operation op = RebaseCommand.Operation.BEGIN; // If other operation other than 'BEGIN' was specified, set it if (request.getOperation() != null) { switch (request.getOperation()) { case REBASE_OPERATION_ABORT: op = RebaseCommand.Operation.ABORT; break; case REBASE_OPERATION_CONTINUE: op = RebaseCommand.Operation.CONTINUE; break; case REBASE_OPERATION_SKIP: op = RebaseCommand.Operation.SKIP; break; default: op = RebaseCommand.Operation.BEGIN; break; } } rebaseCommand.setOperation(op); } @Override public void mv(MoveRequest request) throws GitException { try { getGit().add().addFilepattern(request.getTarget()).call(); getGit().rm().addFilepattern(request.getSource()).call(); } catch (GitAPIException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public PullResponse pull(PullRequest request) throws GitException, UnauthorizedException { String remoteName = request.getRemote(); String remoteUri; try { if (repository.getRepositoryState().equals(RepositoryState.MERGING)) { throw new GitException(ERROR_PULL_MERGING); } String fullBranch = repository.getFullBranch(); if (!fullBranch.startsWith(Constants.R_HEADS)) { throw new DetachedHeadException(ERROR_PULL_HEAD_DETACHED); } String branch = fullBranch.substring(Constants.R_HEADS.length()); StoredConfig config = repository.getConfig(); if (remoteName == null) { remoteName = config.getString(ConfigConstants.CONFIG_BRANCH_SECTION, branch, ConfigConstants.CONFIG_KEY_REMOTE); if (remoteName == null) { remoteName = Constants.DEFAULT_REMOTE_NAME; } } remoteUri = config.getString(ConfigConstants.CONFIG_REMOTE_SECTION, remoteName, ConfigConstants.CONFIG_KEY_URL); String remoteBranch; RefSpec fetchRefSpecs = null; String refSpec = request.getRefSpec(); if (refSpec != null) { fetchRefSpecs = (refSpec.indexOf(':') < 0) // ? new RefSpec(Constants.R_HEADS + refSpec + ":" + fullBranch) // : new RefSpec(refSpec); remoteBranch = fetchRefSpecs.getSource(); } else { remoteBranch = config.getString(ConfigConstants.CONFIG_BRANCH_SECTION, branch, ConfigConstants.CONFIG_KEY_MERGE); } if (remoteBranch == null) { remoteBranch = fullBranch; } FetchCommand fetchCommand = getGit().fetch(); fetchCommand.setRemote(remoteName); if (fetchRefSpecs != null) { fetchCommand.setRefSpecs(fetchRefSpecs); } int timeout = request.getTimeout(); if (timeout > 0) { fetchCommand.setTimeout(timeout); } FetchResult fetchResult = (FetchResult) executeRemoteCommand(remoteUri, fetchCommand); Ref remoteBranchRef = fetchResult.getAdvertisedRef(remoteBranch); if (remoteBranchRef == null) { remoteBranchRef = fetchResult.getAdvertisedRef(Constants.R_HEADS + remoteBranch); } if (remoteBranchRef == null) { throw new GitException(String.format(ERROR_PULL_REF_MISSING, remoteBranch)); } org.eclipse.jgit.api.MergeResult mergeResult = getGit().merge().include(remoteBranchRef).call(); if (mergeResult.getMergeStatus() .equals(org.eclipse.jgit.api.MergeResult.MergeStatus.ALREADY_UP_TO_DATE)) { return newDto(PullResponse.class).withCommandOutput("Already up-to-date"); } if (mergeResult.getConflicts() != null) { StringBuilder message = new StringBuilder(ERROR_PULL_MERGE_CONFLICT_IN_FILES); message.append(lineSeparator()); Map<String, int[][]> allConflicts = mergeResult.getConflicts(); for (String path : allConflicts.keySet()) { message.append(path).append(lineSeparator()); } message.append(ERROR_PULL_AUTO_MERGE_FAILED); throw new GitException(message.toString()); } } catch (CheckoutConflictException exception) { StringBuilder message = new StringBuilder(ERROR_CHECKOUT_CONFLICT); message.append(lineSeparator()); for (String path : exception.getConflictingPaths()) { message.append(path).append(lineSeparator()); } message.append(ERROR_PULL_COMMIT_BEFORE_MERGE); throw new GitException(message.toString(), exception); } catch (IOException | GitAPIException exception) { String errorMessage; if (exception.getMessage().equals("Invalid remote: " + remoteName)) { errorMessage = ERROR_NO_REMOTE_REPOSITORY; } else { errorMessage = exception.getMessage(); } throw new GitException(errorMessage, exception); } return newDto(PullResponse.class).withCommandOutput("Successfully pulled from " + remoteUri); } @Override public PushResponse push(PushRequest request) throws GitException, UnauthorizedException { String remoteName = request.getRemote(); String remoteUri = getRepository().getConfig().getString(ConfigConstants.CONFIG_REMOTE_SECTION, remoteName, ConfigConstants.CONFIG_KEY_URL); PushResponse pushResponseDto = newDto(PushResponse.class); try { PushCommand pushCommand = getGit().push(); if (request.getRemote() != null) { pushCommand.setRemote(remoteName); } List<String> refSpec = request.getRefSpec(); if (!refSpec.isEmpty()) { List<RefSpec> refSpecInst = new ArrayList<>(refSpec.size()); refSpecInst.addAll(refSpec.stream().map(RefSpec::new).collect(Collectors.toList())); pushCommand.setRefSpecs(refSpecInst); } pushCommand.setForce(request.isForce()); int timeout = request.getTimeout(); if (timeout > 0) { pushCommand.setTimeout(timeout); } @SuppressWarnings("unchecked") Iterable<PushResult> pushResults = (Iterable<PushResult>) executeRemoteCommand(remoteUri, pushCommand); PushResult pushResult = pushResults.iterator().next(); return addCommandOutputUpdates(pushResponseDto, request, pushResult); } catch (GitAPIException exception) { if ("origin: not found.".equals(exception.getMessage())) { throw new GitException(ERROR_NO_REMOTE_REPOSITORY, exception); } else { throw new GitException(exception.getMessage(), exception); } } } private PushResponse addCommandOutputUpdates(PushResponse pushResponseDto, final PushRequest request, final PushResult result) throws GitException { String commandOutput = result.getMessages(); final Collection<RemoteRefUpdate> refUpdates = result.getRemoteUpdates(); final List<Map<String, String>> updates = new ArrayList<>(); final String currentBranch = getCurrentBranch(); for (RemoteRefUpdate remoteRefUpdate : refUpdates) { final String remoteRefName = remoteRefUpdate.getRemoteName(); // check status only for branch given in the URL or tags - (handle special "refs/for" case) String shortenRefFor = remoteRefName.startsWith("refs/for/") ? remoteRefName.substring("refs/for/".length()) : remoteRefName; if (currentBranch.equals(Repository.shortenRefName(remoteRefName)) || currentBranch.equals(shortenRefFor) || remoteRefName.startsWith(Constants.R_TAGS)) { Map<String, String> update = new HashMap<>(); RemoteRefUpdate.Status status = remoteRefUpdate.getStatus(); if (status != RemoteRefUpdate.Status.UP_TO_DATE || !remoteRefName.startsWith(Constants.R_TAGS)) { update.put(KEY_COMMIT_MESSAGE, remoteRefUpdate.getMessage()); update.put(KEY_RESULT, status.name()); TrackingRefUpdate refUpdate = remoteRefUpdate.getTrackingRefUpdate(); if (refUpdate != null) { update.put(KEY_REMOTENAME, Repository.shortenRefName(refUpdate.getLocalName())); update.put(KEY_LOCALNAME, Repository.shortenRefName(refUpdate.getRemoteName())); } else { update.put(KEY_REMOTENAME, Repository.shortenRefName(remoteRefUpdate.getSrcRef())); update.put(KEY_LOCALNAME, Repository.shortenRefName(remoteRefUpdate.getRemoteName())); } updates.add(update); } if (status != RemoteRefUpdate.Status.OK) { commandOutput = buildPushFailedMessage(request, remoteRefUpdate, currentBranch); } } } return pushResponseDto.withCommandOutput(commandOutput).withUpdates(updates); } private String buildPushFailedMessage(final PushRequest request, final RemoteRefUpdate remoteRefUpdate, final String currentBranch) { String message; if (remoteRefUpdate.getStatus() == RemoteRefUpdate.Status.UP_TO_DATE) { message = INFO_PUSH_ATTEMPT_IGNORED_UP_TO_DATE; } else { String refspec = currentBranch + BRANCH_REFSPEC_SEPERATOR + request.getRefSpec().get(0).split(REFSPEC_COLON)[1]; if (remoteRefUpdate.getMessage() != null) { message = String.format(ERROR_PUSH_ATTEMPT_FAILED_WITHMSG, refspec, request.getRemote(), remoteRefUpdate.getStatus(), remoteRefUpdate.getMessage()); } else { message = String.format(ERROR_PUSH_ATTEMPT_FAILED_WITHOUTMSG, refspec, request.getRemote(), remoteRefUpdate.getStatus()); } } return message; } @Override public void remoteAdd(RemoteAddRequest request) throws GitException { String remoteName = request.getName(); if (isNullOrEmpty(remoteName)) { throw new IllegalArgumentException(ERROR_ADD_REMOTE_NAME_MISSING); } StoredConfig config = repository.getConfig(); Set<String> remoteNames = config.getSubsections("remote"); if (remoteNames.contains(remoteName)) { throw new IllegalArgumentException(String.format(ERROR_REMOTE_NAME_ALREADY_EXISTS, remoteName)); } String url = request.getUrl(); if (isNullOrEmpty(url)) { throw new IllegalArgumentException(ERROR_REMOTE_URL_MISSING); } RemoteConfig remoteConfig; try { remoteConfig = new RemoteConfig(config, remoteName); } catch (URISyntaxException exception) { // Not happen since it is newly created remote. throw new GitException(exception.getMessage(), exception); } try { remoteConfig.addURI(new URIish(url)); } catch (URISyntaxException exception) { throw new IllegalArgumentException("Remote url " + url + " is invalid. "); } List<String> branches = request.getBranches(); if (branches.isEmpty()) { remoteConfig.addFetchRefSpec( new RefSpec(Constants.R_HEADS + "*" + ":" + Constants.R_REMOTES + remoteName + "/*") .setForceUpdate(true)); } else { for (String branch : branches) { remoteConfig.addFetchRefSpec(new RefSpec( Constants.R_HEADS + branch + ":" + Constants.R_REMOTES + remoteName + "/" + branch) .setForceUpdate(true)); } } remoteConfig.update(config); try { config.save(); } catch (IOException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public void remoteDelete(String name) throws GitException { StoredConfig config = repository.getConfig(); Set<String> remoteNames = config.getSubsections(ConfigConstants.CONFIG_KEY_REMOTE); if (!remoteNames.contains(name)) { throw new GitException("error: Could not remove config section 'remote." + name + "'"); } config.unsetSection(ConfigConstants.CONFIG_REMOTE_SECTION, name); Set<String> branches = config.getSubsections(ConfigConstants.CONFIG_BRANCH_SECTION); for (String branch : branches) { String r = config.getString(ConfigConstants.CONFIG_BRANCH_SECTION, branch, ConfigConstants.CONFIG_KEY_REMOTE); if (name.equals(r)) { config.unset(ConfigConstants.CONFIG_BRANCH_SECTION, branch, ConfigConstants.CONFIG_KEY_REMOTE); config.unset(ConfigConstants.CONFIG_BRANCH_SECTION, branch, ConfigConstants.CONFIG_KEY_MERGE); List<Branch> remoteBranches = branchList(newDto(BranchListRequest.class).withListMode("r")); for (Branch remoteBranch : remoteBranches) { if (remoteBranch.getDisplayName().startsWith(name)) { branchDelete( newDto(BranchDeleteRequest.class).withName(remoteBranch.getName()).withForce(true)); } } } } try { config.save(); } catch (IOException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public List<Remote> remoteList(RemoteListRequest request) throws GitException { StoredConfig config = repository.getConfig(); Set<String> remoteNames = new HashSet<>(config.getSubsections(ConfigConstants.CONFIG_KEY_REMOTE)); String remote = request.getRemote(); if (remote != null && remoteNames.contains(remote)) { remoteNames.clear(); remoteNames.add(remote); } List<Remote> result = new ArrayList<>(remoteNames.size()); for (String remoteName : remoteNames) { try { List<URIish> uris = new RemoteConfig(config, remoteName).getURIs(); result.add(newDto(Remote.class).withName(remoteName) .withUrl(uris.isEmpty() ? null : uris.get(0).toString())); } catch (URISyntaxException exception) { throw new GitException(exception.getMessage(), exception); } } return result; } @Override public void remoteUpdate(RemoteUpdateRequest request) throws GitException { String remoteName = request.getName(); if (isNullOrEmpty(remoteName)) { throw new IllegalArgumentException(ERROR_UPDATE_REMOTE_NAME_MISSING); } StoredConfig config = repository.getConfig(); Set<String> remoteNames = config.getSubsections(ConfigConstants.CONFIG_KEY_REMOTE); if (!remoteNames.contains(remoteName)) { throw new IllegalArgumentException("Remote " + remoteName + " not found. "); } RemoteConfig remoteConfig; try { remoteConfig = new RemoteConfig(config, remoteName); } catch (URISyntaxException e) { throw new GitException(e.getMessage(), e); } List<String> branches = request.getBranches(); if (!branches.isEmpty()) { if (!request.isAddBranches()) { remoteConfig.setFetchRefSpecs(Collections.emptyList()); remoteConfig.setPushRefSpecs(Collections.emptyList()); } else { // Replace wildcard refSpec if any. remoteConfig.removeFetchRefSpec( new RefSpec(Constants.R_HEADS + "*" + ":" + Constants.R_REMOTES + remoteName + "/*") .setForceUpdate(true)); remoteConfig.removeFetchRefSpec( new RefSpec(Constants.R_HEADS + "*" + ":" + Constants.R_REMOTES + remoteName + "/*")); } // Add new refSpec. for (String branch : branches) { remoteConfig.addFetchRefSpec(new RefSpec( Constants.R_HEADS + branch + ":" + Constants.R_REMOTES + remoteName + "/" + branch) .setForceUpdate(true)); } } // Remove URLs first. for (String url : request.getRemoveUrl()) { try { remoteConfig.removeURI(new URIish(url)); } catch (URISyntaxException e) { LOG.debug(ERROR_REMOVING_INVALID_URL); } } // Add new URLs. for (String url : request.getAddUrl()) { try { remoteConfig.addURI(new URIish(url)); } catch (URISyntaxException e) { throw new IllegalArgumentException("Remote url " + url + " is invalid. "); } } // Remove URLs for pushing. for (String url : request.getRemovePushUrl()) { try { remoteConfig.removePushURI(new URIish(url)); } catch (URISyntaxException e) { LOG.debug(ERROR_REMOVING_INVALID_URL); } } // Add URLs for pushing. for (String url : request.getAddPushUrl()) { try { remoteConfig.addPushURI(new URIish(url)); } catch (URISyntaxException e) { throw new IllegalArgumentException("Remote push url " + url + " is invalid. "); } } remoteConfig.update(config); try { config.save(); } catch (IOException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public void reset(ResetRequest request) throws GitException { try { ResetCommand resetCommand = getGit().reset(); resetCommand.setRef(request.getCommit()); List<String> patterns = request.getFilePattern(); patterns.stream().forEach(resetCommand::addPath); if (request.getType() != null && patterns.isEmpty()) { switch (request.getType()) { case HARD: resetCommand.setMode(ResetType.HARD); break; case KEEP: resetCommand.setMode(ResetType.KEEP); break; case MERGE: resetCommand.setMode(ResetType.MERGE); break; case MIXED: resetCommand.setMode(ResetType.MIXED); break; case SOFT: resetCommand.setMode(ResetType.SOFT); break; } } resetCommand.call(); } catch (GitAPIException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public void rm(RmRequest request) throws GitException { List<String> files = request.getItems(); RmCommand rmCommand = getGit().rm(); rmCommand.setCached(request.isCached()); if (files != null) { files.forEach(rmCommand::addFilepattern); } try { rmCommand.call(); } catch (GitAPIException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public Status status(StatusFormat format) throws GitException { if (!RepositoryCache.FileKey.isGitRepository(getRepository().getDirectory(), FS.DETECTED)) { throw new GitException("Not a git repository"); } String branchName = getCurrentBranch(); return new JGitStatusImpl(branchName, getGit().status(), format); } @Override public Tag tagCreate(TagCreateRequest request) throws GitException { String commit = request.getCommit(); if (commit == null) { commit = Constants.HEAD; } try { RevWalk revWalk = new RevWalk(repository); RevObject revObject; try { revObject = revWalk.parseAny(repository.resolve(commit)); } finally { revWalk.close(); } TagCommand tagCommand = getGit().tag().setName(request.getName()).setObjectId(revObject) .setMessage(request.getMessage()).setForceUpdate(request.isForce()); GitUser tagger = getUser(); if (tagger != null) { tagCommand.setTagger(new PersonIdent(tagger.getName(), tagger.getEmail())); } Ref revTagRef = tagCommand.call(); RevTag revTag = revWalk.parseTag(revTagRef.getLeaf().getObjectId()); return newDto(Tag.class).withName(revTag.getTagName()); } catch (IOException | GitAPIException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public void tagDelete(TagDeleteRequest request) throws GitException { try { String tagName = request.getName(); Ref tagRef = repository.findRef(tagName); if (tagRef == null) { throw new IllegalArgumentException("Tag " + tagName + " not found. "); } RefUpdate updateRef = repository.updateRef(tagRef.getName()); updateRef.setRefLogMessage("tag deleted", false); updateRef.setForceUpdate(true); Result deleteResult = updateRef.delete(); if (deleteResult != Result.FORCED && deleteResult != Result.FAST_FORWARD) { throw new GitException(String.format(ERROR_TAG_DELETE, tagName, deleteResult)); } } catch (IOException exception) { throw new GitException(exception.getMessage(), exception); } } @Override public List<Tag> tagList(TagListRequest request) throws GitException { String patternStr = request.getPattern(); Pattern pattern = null; if (patternStr != null) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < patternStr.length(); i++) { char c = patternStr.charAt(i); if (c == '*' || c == '?') { sb.append('.'); } else if (c == '.' || c == '(' || c == ')' || c == '[' || c == ']' || c == '^' || c == '$' || c == '|') { sb.append('\\'); } sb.append(c); } pattern = Pattern.compile(sb.toString()); } Set<String> tagNames = repository.getTags().keySet(); List<Tag> tags = new ArrayList<>(tagNames.size()); for (String tagName : tagNames) { if (pattern == null || pattern.matcher(tagName).matches()) { tags.add(newDto(Tag.class).withName(tagName)); } } return tags; } @Override public void close() { repository.close(); } @Override public File getWorkingDir() { return repository.getWorkTree(); } @Override public List<RemoteReference> lsRemote(LsRemoteRequest request) throws UnauthorizedException, GitException { String remoteUrl = request.getRemoteUrl(); LsRemoteCommand lsRemoteCommand = getGit().lsRemote().setRemote(remoteUrl); Collection<Ref> refs; try { refs = lsRemoteCommand.call(); } catch (GitAPIException exception) { if (exception.getMessage().contains(ERROR_AUTHENTICATION_REQUIRED)) { throw new UnauthorizedException(String.format(ERROR_AUTHENTICATION_FAILED, remoteUrl)); } else { throw new GitException(exception.getMessage(), exception); } } return refs.stream().map(ref -> newDto(RemoteReference.class).withCommitId(ref.getObjectId().name()) .withReferenceName(ref.getName())).collect(Collectors.toList()); } @Override public Config getConfig() throws GitException { if (config != null) { return config; } return config = new JGitConfigImpl(repository); } @Override public void setOutputLineConsumerFactory(LineConsumerFactory outputPublisherFactory) { //nothing to do, not outputs are produced by JGit } private Git getGit() { if (git != null) { return git; } return git = new Git(repository); } @Override public List<String> listFiles(LsFilesRequest request) throws GitException { return Arrays.asList(getWorkingDir().list(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return !name.startsWith("."); } })); } @Override public void cloneWithSparseCheckout(String directory, String remoteUrl, String branch) throws GitException, UnauthorizedException { //TODO rework this code when jgit will support sparse-checkout. Tracked issue: https://bugs.eclipse.org/bugs/show_bug.cgi?id=383772 clone(newDto(CloneRequest.class).withRemoteUri(remoteUrl)); if (!"master".equals(branch)) { checkout(newDto(CheckoutRequest.class).withName(branch)); } final String sourcePath = getWorkingDir().getPath(); final String keepDirectoryPath = sourcePath + "/" + directory; IOFileFilter folderFilter = new DirectoryFileFilter() { public boolean accept(File dir) { String directoryPath = dir.getPath(); return !(directoryPath.startsWith(keepDirectoryPath) || directoryPath.startsWith(sourcePath + "/.git")); } }; Collection<File> files = org.apache.commons.io.FileUtils.listFilesAndDirs(getWorkingDir(), TrueFileFilter.INSTANCE, folderFilter); try { DirCache index = getRepository().lockDirCache(); int sourcePathLength = sourcePath.length() + 1; files.stream().filter(File::isFile).forEach( file -> index.getEntry(file.getPath().substring(sourcePathLength)).setAssumeValid(true)); index.write(); index.commit(); for (File file : files) { if (keepDirectoryPath.startsWith(file.getPath())) { continue; } if (file.exists()) { FileUtils.delete(file, FileUtils.RECURSIVE); } } } catch (IOException exception) { throw new GitException(exception.getMessage(), exception); } } /** * Execute remote jgit command. * * @param remoteUrl * remote url * @param command * command to execute * @return executed command * @throws GitException * @throws GitAPIException * @throws UnauthorizedException */ private Object executeRemoteCommand(String remoteUrl, TransportCommand command) throws GitException, GitAPIException, UnauthorizedException { String sshKeyDirectoryPath = ""; try { if (GitUrlUtils.isSSH(remoteUrl)) { File keyDirectory = Files.createTempDir(); sshKeyDirectoryPath = keyDirectory.getPath(); File sshKey = writePrivateKeyFile(remoteUrl, keyDirectory); SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() { @Override protected void configure(OpenSshConfig.Host host, Session session) { session.setConfig("StrictHostKeyChecking", "no"); } @Override protected JSch getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException { JSch jsch = super.getJSch(hc, fs); jsch.removeAllIdentity(); jsch.addIdentity(sshKey.getAbsolutePath()); return jsch; } }; command.setTransportConfigCallback(new TransportConfigCallback() { @Override public void configure(Transport transport) { SshTransport sshTransport = (SshTransport) transport; sshTransport.setSshSessionFactory(sshSessionFactory); } }); } else { UserCredential credentials = credentialsLoader.getUserCredential(remoteUrl); if (credentials != null) { command.setCredentialsProvider(new UsernamePasswordCredentialsProvider( credentials.getUserName(), credentials.getPassword())); } } return command.call(); } catch (GitException | TransportException exception) { if ("Unable get private ssh key".equals(exception.getMessage()) || exception.getMessage().contains(ERROR_AUTHENTICATION_REQUIRED)) { throw new UnauthorizedException(exception.getMessage()); } else { throw exception; } } finally { if (!sshKeyDirectoryPath.isEmpty()) { try { FileUtils.delete(new File(sshKeyDirectoryPath), FileUtils.RECURSIVE); } catch (IOException exception) { throw new GitException("Can't remove SSH key directory", exception); } } } } /** * Writes private SSH key into file. * * @return file that contains SSH key * @throws GitException * if other error occurs */ private File writePrivateKeyFile(String url, File keyDirectory) throws GitException { final File keyFile = new File(keyDirectory, "identity"); try (FileOutputStream fos = new FileOutputStream(keyFile)) { byte[] sshKey = sshKeyProvider.getPrivateKey(url); fos.write(sshKey); } catch (IOException | ServerException exception) { String errorMessage = "Can't store ssh key. ".concat(exception.getMessage()); LOG.error(errorMessage, exception); throw new GitException(errorMessage, exception); } Set<PosixFilePermission> permissions = EnumSet.of(OWNER_READ, OWNER_WRITE); try { java.nio.file.Files.setPosixFilePermissions(keyFile.toPath(), permissions); } catch (IOException exception) { throw new GitException(exception.getMessage(), exception); } return keyFile; } private GitUser getUser() throws GitException { return userResolver.getUser(); } private void deleteRepositoryFolder() { try { if (repository.getDirectory().exists()) { FileUtils.delete(repository.getDirectory(), FileUtils.RECURSIVE | FileUtils.IGNORE_ERRORS); } } catch (Exception exception) { // Ignore the error since we want to throw the original error LOG.error("Could not remove .git folder in path " + repository.getDirectory().getPath(), exception); } } private Repository getRepository() { return repository; } private String getCurrentBranch() throws GitException { try { return Repository.shortenRefName(repository.exactRef(Constants.HEAD).getLeaf().getName()); } catch (IOException exception) { throw new GitException(exception.getMessage(), exception); } } /** * Method for cleaning name of remote branch to be checked out. I.e. it * takes something like "origin/testBranch" and returns "testBranch". This * is needed for view-compatibility with console Git client. * * @param branchName * is a name of branch to be cleaned * @return branchName without remote repository name * @throws GitException */ private String cleanRemoteName(String branchName) throws GitException { String returnName = branchName; List<Remote> remotes = this.remoteList(newDto(RemoteListRequest.class)); for (Remote remote : remotes) { if (branchName.startsWith(remote.getName())) { returnName = branchName.replaceFirst(remote.getName() + "/", ""); } } return returnName; } }