| /******************************************************************************* |
| * Copyright (c) 2011 Oak Ridge National Laboratory and others. |
| * 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: |
| * John Eblen - initial implementation |
| *******************************************************************************/ |
| package org.eclipse.ptp.rdt.sync.git.core; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.StringReader; |
| import java.net.URISyntaxException; |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.jgit.api.AddCommand; |
| import org.eclipse.jgit.api.CommitCommand; |
| import org.eclipse.jgit.api.Git; |
| import org.eclipse.jgit.api.MergeCommand; |
| import org.eclipse.jgit.api.errors.CheckoutConflictException; |
| import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; |
| import org.eclipse.jgit.api.errors.GitAPIException; |
| import org.eclipse.jgit.api.errors.InvalidMergeHeadsException; |
| import org.eclipse.jgit.api.errors.NoFilepatternException; |
| import org.eclipse.jgit.api.errors.NoHeadException; |
| import org.eclipse.jgit.api.errors.NoMessageException; |
| import org.eclipse.jgit.api.errors.WrongRepositoryStateException; |
| import org.eclipse.jgit.errors.NotSupportedException; |
| import org.eclipse.jgit.errors.TransportException; |
| import org.eclipse.jgit.errors.UnmergedPathException; |
| import org.eclipse.jgit.lib.NullProgressMonitor; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.lib.StoredConfig; |
| import org.eclipse.jgit.storage.file.FileRepositoryBuilder; |
| import org.eclipse.jgit.transport.CredentialsProvider; |
| import org.eclipse.jgit.transport.RefSpec; |
| import org.eclipse.jgit.transport.RemoteConfig; |
| import org.eclipse.jgit.transport.RemoteSession; |
| import org.eclipse.jgit.transport.SshSessionFactory; |
| import org.eclipse.jgit.transport.Transport; |
| import org.eclipse.jgit.transport.TransportGitSsh; |
| import org.eclipse.jgit.transport.URIish; |
| import org.eclipse.jgit.util.FS; |
| import org.eclipse.ptp.rdt.sync.git.core.CommandRunner.CommandResults; |
| import org.eclipse.ptp.remote.core.AbstractRemoteProcess; |
| import org.eclipse.ptp.remote.core.IRemoteConnection; |
| import org.eclipse.ptp.remote.core.exception.RemoteConnectionException; |
| |
| /** |
| * |
| * This class implements a remote sync tool using git, as accessed through the jgit library. |
| * |
| */ |
| public class GitRemoteSyncConnection { |
| private final static String remoteProjectName = "eclipse_auto"; //$NON-NLS-1$ |
| private final static String commitMessage = "Eclipse Automatic Commit"; //$NON-NLS-1$ |
| private final static String remotePushBranch = "ptp-push"; //$NON-NLS-1$ |
| private final IRemoteConnection connection; |
| private final String localDirectory; |
| private final String remoteDirectory; |
| private Git git; |
| private TransportGitSsh transport; |
| |
| /** |
| * Create a remote sync connection using git. Assumes that the local directory exists but not necessarily the remote directory. |
| * It is created if not. |
| * |
| * @param conn |
| * @param localDir |
| * @param remoteDir |
| * @throws RemoteSyncException |
| * on problems building the remote repository. Specific exception nested. Upon such an exception, the instance is |
| * invalid and should not be used. |
| */ |
| public GitRemoteSyncConnection(IRemoteConnection conn, String localDir, String remoteDir) throws RemoteSyncException { |
| connection = conn; |
| localDirectory = localDir; |
| remoteDirectory = remoteDir; |
| |
| // Build repo, creating it if it is not already present. |
| try { |
| buildRepo(); |
| } catch (final CoreException e) { |
| throw new RemoteSyncException(e); |
| } catch (final IOException e) { |
| throw new RemoteSyncException(e); |
| } catch (final RemoteExecutionException e) { |
| throw new RemoteSyncException(e); |
| } |
| |
| // Build transport |
| final RemoteConfig remoteConfig = buildRemoteConfig(git.getRepository().getConfig()); |
| buildTransport(remoteConfig); |
| } |
| |
| /** |
| * Builds the remote configuration for the connection, setting up fetch and push operations between local and remote master |
| * branches. |
| * |
| * @param config |
| * configuration for the local repository |
| * @return the remote configuration |
| * @throws RuntimeException |
| * if the URI in the passed configuration is not properly formatted. |
| */ |
| private RemoteConfig buildRemoteConfig(StoredConfig config) { |
| RemoteConfig rconfig = null; |
| |
| try { |
| rconfig = new RemoteConfig(config, remoteProjectName); |
| } catch (final URISyntaxException e) { |
| throw new RuntimeException(e); |
| } |
| |
| final RefSpec refSpecFetch = new RefSpec("+refs/heads/master:refs/remotes/" + //$NON-NLS-1$ |
| remoteProjectName + "/master"); //$NON-NLS-1$ |
| final RefSpec refSpecPush = new RefSpec("+master:" + remotePushBranch); //$NON-NLS-1$ |
| rconfig.addFetchRefSpec(refSpecFetch); |
| rconfig.addPushRefSpec(refSpecPush); |
| |
| return rconfig; |
| } |
| |
| /** |
| * |
| * @param localDirectory |
| * @param remoteHost |
| * @return the repository |
| * @throws CoreException |
| * on problems creating the remote directory. |
| * @throws IOException |
| * on problems writing to the file system. |
| * @throws RemoteExecutionException |
| * on failure to run remote commands. |
| * @throws RemoteSyncException |
| * on problems with initial local commit. |
| * TODO: Consider the consequences of exceptions that occur at various points, which can leave the repo in a partial state. |
| * For example, if the repo is created but the initial commit fails. |
| * TODO: Consider evaluating the output of "git init". |
| */ |
| private Git buildRepo() throws CoreException, IOException, RemoteExecutionException, RemoteSyncException { |
| final File localDir = new File(localDirectory); |
| final FileRepositoryBuilder repoBuilder = new FileRepositoryBuilder(); |
| Repository repository = null; |
| |
| try { |
| repository = repoBuilder.setWorkTree(localDir).build(); |
| } catch (final IOException e) { |
| throw e; |
| } |
| git = new Git(repository); |
| |
| // Create and configure local repository if it is not already present. Set the git instance. |
| if (repoReady() == false) { |
| try { |
| repository.create(false); |
| } catch (final IOException e) { |
| throw e; |
| } |
| |
| // An initial commit to create the master branch. |
| try { |
| doCommit(); |
| } catch (final RemoteSyncException e) { |
| throw e; |
| } |
| } |
| |
| // Create remote directory if necessary. |
| try { |
| CommandRunner.createRemoteDirectory(connection, remoteDirectory); |
| } catch (final CoreException e) { |
| throw e; |
| } |
| |
| // Initialize remote directory if necessary |
| try { |
| doRemoteInit(); |
| } catch (final IOException e) { |
| throw e; |
| } catch (final RemoteExecutionException e) { |
| throw e; |
| } |
| |
| // Commit remote files if necessary |
| try { |
| doRemoteCommit(); |
| } catch (final IOException e) { |
| throw e; |
| } catch (final RemoteExecutionException e) { |
| throw e; |
| } |
| |
| return new Git(repository); |
| } |
| |
| /** |
| * Create and configure remote repository if it is not already present. Note that "git init" is "safe" on a repo already |
| * created, so we can simply rerun it each time. |
| * @throws IOException |
| * @throws RemoteExecutionException |
| */ |
| private void doRemoteInit() throws IOException, RemoteExecutionException { |
| String command = null; |
| CommandResults commandResults = null; |
| |
| command = "git init"; //$NON-NLS-1$ |
| try { |
| commandResults = CommandRunner.executeRemoteCommand(connection, command, remoteDirectory, null); |
| } catch (final IOException e) { |
| throw e; |
| } catch (final InterruptedException e) { |
| throw new RemoteExecutionException(e); |
| } catch (RemoteConnectionException e) { |
| throw new RemoteExecutionException(e); |
| } |
| |
| if (commandResults.getExitCode() != 0) { |
| throw new RemoteExecutionException("remote git init failed with message: " + //$NON-NLS-1$ |
| commandResults.getStderr()); |
| } |
| } |
| |
| /* |
| * Do a commit on the remote repository. First we add and delete files from the git index as needed, and then we call "git commit" |
| * TODO: Modified files already added by "git add" will not be found by "getRemoteFileStatus". Thus, a commit may not happen even |
| * though there are outstanding changes. Note that this can only occur by accessing the repo outside of Eclipse. |
| */ |
| private void doRemoteCommit() throws IOException, RemoteExecutionException { |
| Set<String> filesToAdd = new HashSet<String>(); |
| Set<String> filesToDelete = new HashSet<String>(); |
| boolean needToCommit = false; |
| |
| try { |
| getRemoteFileStatus(filesToAdd, filesToDelete); |
| for (String fileName : filesToDelete) { |
| if (filesToAdd.contains(fileName)) { |
| filesToAdd.remove(fileName); |
| } |
| } |
| if (filesToAdd.size() > 0) { |
| addRemoteFiles(filesToAdd); |
| needToCommit = true; |
| } |
| if (filesToDelete.size() > 0) { |
| deleteRemoteFiles(filesToDelete); |
| needToCommit = true; |
| } |
| |
| if (needToCommit) { |
| commitRemoteFiles(); |
| } |
| } catch (IOException e) { |
| throw e; |
| } catch (RemoteExecutionException e) { |
| throw e; |
| } |
| } |
| |
| /* |
| * Do a "git commit" on the remote host |
| */ |
| private void commitRemoteFiles() throws IOException, RemoteExecutionException { |
| final String command = "git commit -m \"" + commitMessage + "\""; //$NON-NLS-1$ //$NON-NLS-2$ |
| CommandResults commandResults = null; |
| |
| try { |
| commandResults = CommandRunner.executeRemoteCommand(connection, command, remoteDirectory, null); |
| } catch (final IOException e) { |
| throw e; |
| } catch (final InterruptedException e) { |
| throw new RemoteExecutionException(e); |
| } catch (RemoteConnectionException e) { |
| throw new RemoteExecutionException(e); |
| } |
| if (commandResults.getExitCode() != 0) { |
| throw new RemoteExecutionException("remote git commit failed with message: " + //$NON-NLS-1$ |
| commandResults.getStderr()); |
| } |
| } |
| |
| /* |
| * Do a "git rm <Files>" on the remote host |
| */ |
| private void deleteRemoteFiles(Set<String> filesToDelete) throws IOException, RemoteExecutionException { |
| String command = "git rm"; //$NON-NLS-1$ |
| for (String fileName : filesToDelete) { |
| command = command.concat(" "); //$NON-NLS-1$ |
| command = command.concat(fileName); |
| } |
| |
| CommandResults commandResults = null; |
| try { |
| commandResults = CommandRunner.executeRemoteCommand(connection, command, remoteDirectory, null); |
| } catch (final IOException e) { |
| throw e; |
| } catch (final InterruptedException e) { |
| throw new RemoteExecutionException(e); |
| } catch (RemoteConnectionException e) { |
| throw new RemoteExecutionException(e); |
| } |
| if (commandResults.getExitCode() != 0) { |
| throw new RemoteExecutionException("remote git rm failed with message: " + //$NON-NLS-1$ |
| commandResults.getStderr()); |
| } |
| } |
| |
| /* |
| * Do a "git add <Files>" on the remote host |
| */ |
| private void addRemoteFiles(Set<String> filesToAdd) throws IOException, RemoteExecutionException { |
| String command = "git add"; //$NON-NLS-1$ |
| for (String fileName : filesToAdd) { |
| command = command.concat(" "); //$NON-NLS-1$ |
| command = command.concat(fileName); |
| } |
| |
| CommandResults commandResults = null; |
| try { |
| commandResults = CommandRunner.executeRemoteCommand(connection, command, remoteDirectory, null); |
| } catch (final IOException e) { |
| throw e; |
| } catch (final InterruptedException e) { |
| throw new RemoteExecutionException(e); |
| } catch (RemoteConnectionException e) { |
| throw new RemoteExecutionException(e); |
| } |
| if (commandResults.getExitCode() != 0) { |
| throw new RemoteExecutionException("remote git add failed with message: " + //$NON-NLS-1$ |
| commandResults.getStderr()); |
| } |
| } |
| |
| /* |
| * Use "git ls-files" to obtain a list of files that need to be added or deleted from the git index. |
| */ |
| private void getRemoteFileStatus(Set<String> filesToAdd, Set<String> filesToDelete) |
| throws IOException, RemoteExecutionException { |
| final String command = "git ls-files -t --modified --others --deleted"; //$NON-NLS-1$ |
| CommandResults commandResults = null; |
| |
| try { |
| commandResults = CommandRunner.executeRemoteCommand(connection, command, remoteDirectory, null); |
| } catch (final IOException e) { |
| throw e; |
| } catch (final InterruptedException e) { |
| throw new RemoteExecutionException(e); |
| } catch (RemoteConnectionException e) { |
| throw new RemoteExecutionException(e); |
| } |
| if (commandResults.getExitCode() != 0) { |
| throw new RemoteExecutionException("remote git ls-files failed with message: " + //$NON-NLS-1$ |
| commandResults.getStderr()); |
| } |
| |
| BufferedReader statusReader = new BufferedReader(new StringReader(commandResults.getStdout())); |
| String line = null; |
| while ((line = statusReader.readLine()) != null) { |
| String[] lineParts = line.split("\\s+"); //$NON-NLS-1$ |
| if (lineParts.length < 2) { |
| continue; |
| } |
| if (lineParts[0].startsWith("R")) { //$NON-NLS-1$ |
| filesToDelete.add(lineParts[1]); |
| } else { |
| filesToAdd.add(lineParts[1]); |
| } |
| } |
| statusReader.close(); |
| } |
| |
| // Subclass JGit's generic RemoteSession to set up running of remote commands using the available process builder. |
| public class PTPSession implements RemoteSession { |
| private URIish uri; |
| |
| public PTPSession(URIish uri) { |
| this.uri = uri; |
| } |
| |
| public Process exec(String command, int timeout) throws TransportException { |
| // TODO: Use a library for command splitting. |
| List<String> commandList = new LinkedList<String>(); |
| commandList.add("sh"); //$NON-NLS-1$ |
| commandList.add("-c"); //$NON-NLS-1$ |
| commandList.add(command); |
| |
| try { |
| if (!connection.isOpen()) { |
| connection.open(null); |
| } |
| return (AbstractRemoteProcess) connection.getRemoteServices().getProcessBuilder(connection, commandList).start(); |
| } catch (IOException e) { |
| throw new TransportException(uri, e.getMessage(), e); |
| } catch (RemoteConnectionException e) { |
| throw new TransportException(uri, e.getMessage(), e); |
| } |
| |
| } |
| |
| public void disconnect() { |
| // Nothing to do |
| } |
| } |
| |
| /** |
| * Creates the transport object that JGit uses for executing commands remotely. |
| * |
| * @param remoteConfig |
| * the remote configuration for our local Git repo |
| * @return the transport |
| * @throws RuntimeException |
| * if the requested transport is not supported by JGit. |
| */ |
| private void buildTransport(RemoteConfig remoteConfig) { |
| final URIish uri = buildURI(); |
| try { |
| transport = (TransportGitSsh) Transport.open(git.getRepository(), uri); |
| } catch (NotSupportedException e) { |
| throw new RuntimeException(e); |
| } catch (TransportException e) { |
| throw new RuntimeException(e); |
| } |
| |
| // Set the transport to use our own means of executing remote commands. |
| transport.setSshSessionFactory(new SshSessionFactory() { |
| @Override |
| public RemoteSession getSession(URIish uri, CredentialsProvider credentialsProvider, FS fs, int tms) throws TransportException { |
| return new PTPSession(uri); |
| } |
| }); |
| |
| transport.applyConfig(remoteConfig); |
| } |
| |
| /** |
| * Build the URI for the remote host as needed by the transport. Since the transport will use an external SSH session, we do |
| * not need to provide user, host, or password. However, the function for opening a transport throws an exception if the host |
| * is null or empty length. So we set it to a dummy string. |
| * |
| * @return URIish |
| */ |
| private URIish buildURI() { |
| return new URIish() |
| // .setUser(connection.getUsername()) |
| .setHost("/not/a/real/host") //$NON-NLS-1$ |
| // .setPass("") |
| .setScheme("ssh") //$NON-NLS-1$ |
| .setPath(remoteDirectory); |
| } |
| |
| public void close() { |
| transport.close(); |
| } |
| |
| /** |
| * Commits files in working directory. For now, we just commit all files. So adding ".", handles all files, including newly |
| * created files, and setting the all flag (-a) ensures that deleted files are updated. |
| * TODO: Figure out how to do this more efficiently, as was done remotely (using git ls-files) |
| * |
| * @throws RemoteSyncException |
| * on problems committing. |
| */ |
| private void doCommit() throws RemoteSyncException { |
| final AddCommand addCommand = git.add(); |
| addCommand.addFilepattern("."); //$NON-NLS-1$ |
| try { |
| addCommand.call(); |
| } catch (final NoFilepatternException e) { |
| throw new RemoteSyncException(e); |
| } |
| |
| final CommitCommand commitCommand = git.commit(); |
| commitCommand.setAll(true); |
| commitCommand.setMessage(commitMessage); |
| |
| try { |
| commitCommand.call(); |
| } catch (final GitAPIException e) { |
| throw new RemoteSyncException(e); |
| } catch (final UnmergedPathException e) { |
| throw new RemoteSyncException(e); |
| } |
| } |
| |
| /** |
| * @return the connection (IRemoteConnection) |
| */ |
| public IRemoteConnection getConnection() { |
| return connection; |
| } |
| |
| /** |
| * @return the localDirectory |
| */ |
| public String getLocalDirectory() { |
| return localDirectory; |
| } |
| |
| /** |
| * @return the remoteDirectory |
| */ |
| public String getRemoteDirectory() { |
| return remoteDirectory; |
| } |
| |
| /** |
| * |
| * @param localDirectory |
| * @return If the repo has actually been initialized |
| * TODO: Consider the ways this could go wrong. What if the directory name already ends in a slash? What if ".git" is a file or |
| * does not contain the appropriate files? |
| */ |
| private boolean repoReady() { |
| final String repoDirectory = localDirectory + "/.git"; //$NON-NLS-1$ |
| final File repoDir = new File(repoDirectory); |
| return repoDir.exists(); |
| } |
| |
| /** |
| * Many of the listed exceptions appear to be unrecoverable, caused by errors in the initial setup. It is vital, though, that |
| * failed syncs are reported and handled. So all exceptions are checked exceptions, embedded in a RemoteSyncException. |
| * |
| * @throws RemoteSyncException |
| * for various problems sync'ing. The specific exception is nested within the RemoteSyncException. |
| * TODO: Consider possible platform dependency. |
| */ |
| public void syncLocalToRemote() throws RemoteSyncException { |
| // First commit changes to the local repository. |
| try { |
| doCommit(); |
| } catch (final RemoteSyncException e) { |
| throw e; |
| } |
| |
| // Then push them to the remote site. |
| try { |
| transport.push(NullProgressMonitor.INSTANCE, null); |
| } catch (final NotSupportedException e) { |
| throw new RemoteSyncException(e); |
| } catch (final TransportException e) { |
| throw new RemoteSyncException(e); |
| } |
| |
| // Now remotely merge changes with master branch |
| CommandResults mergeResults; |
| final String command = "git merge " + remotePushBranch; //$NON-NLS-1$ |
| try { |
| mergeResults = CommandRunner.executeRemoteCommand(connection, command, remoteDirectory, null); |
| } catch (final IOException e) { |
| throw new RemoteSyncException(e); |
| } catch (final InterruptedException e) { |
| throw new RemoteSyncException(e); |
| } catch (RemoteConnectionException e) { |
| throw new RemoteSyncException(e); |
| } |
| |
| if (mergeResults.getExitCode() != 0) { |
| throw new RemoteSyncException(new RemoteExecutionException("Remote merge failed with message: " + //$NON-NLS-1$ |
| mergeResults.getStderr())); |
| } |
| } |
| |
| /** |
| * @throws RemoteSyncException |
| * for various problems sync'ing. The specific exception is nested within the RemoteSyncException. Many of the |
| * listed exceptions appear to be unrecoverable, caused by errors in the initial setup. It is vital, though, that |
| * failed syncs are reported and handled. So all exceptions are checked exceptions, embedded in a |
| * RemoteSyncException. |
| */ |
| public void syncRemoteToLocal() throws RemoteSyncException { |
| |
| // TODO: Figure out why pull doesn't work and why we have to fetch and merge instead. |
| // PullCommand pullCommand = gitConnection.getGit().pull(). |
| // try { |
| // pullCommand.call(); |
| // } catch (WrongRepositoryStateException e) { |
| // throw new RemoteSyncException(e); |
| // } catch (InvalidConfigurationException e) { |
| // throw new RemoteSyncException(e); |
| // } catch (DetachedHeadException e) { |
| // throw new RemoteSyncException(e); |
| // } catch (InvalidRemoteException e) { |
| // throw new RemoteSyncException(e); |
| // } catch (CanceledException e) { |
| // throw new RemoteSyncException(e); |
| // } |
| |
| // First, commit in case any changes have occurred remotely. |
| try { |
| doRemoteCommit(); |
| } catch (IOException e) { |
| throw new RemoteSyncException(e); |
| } catch (RemoteExecutionException e) { |
| throw new RemoteSyncException(e); |
| } |
| |
| // Next, fetch the remote repository |
| try { |
| transport.fetch(NullProgressMonitor.INSTANCE, null); |
| } catch (final NotSupportedException e) { |
| throw new RemoteSyncException(e); |
| } catch (final TransportException e) { |
| throw new RemoteSyncException(e); |
| } |
| |
| // Now merge. Before merging we set the head for merging to master. |
| Ref masterRef = null; |
| try { |
| masterRef = git.getRepository().getRef("refs/remotes/" + remoteProjectName + "/master"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } catch (final IOException e) { |
| throw new RemoteSyncException(e); |
| } |
| |
| final MergeCommand mergeCommand = git.merge().include(masterRef); |
| try { |
| mergeCommand.call(); |
| } catch (final NoHeadException e) { |
| throw new RemoteSyncException(e); |
| } catch (final ConcurrentRefUpdateException e) { |
| throw new RemoteSyncException(e); |
| } catch (final CheckoutConflictException e) { |
| throw new RemoteSyncException(e); |
| } catch (final InvalidMergeHeadsException e) { |
| throw new RemoteSyncException(e); |
| } catch (final WrongRepositoryStateException e) { |
| throw new RemoteSyncException(e); |
| } catch (final NoMessageException e) { |
| throw new RemoteSyncException(e); |
| } |
| } |
| } |