| /******************************************************************************* |
| * Copyright (c) 2011, 2014 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.internal.rdt.sync.git.core; |
| |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ConcurrentMap; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicLong; |
| import java.util.concurrent.locks.ReentrantLock; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IResourceDelta; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.jgit.api.errors.GitAPIException; |
| import org.eclipse.jgit.errors.TransportException; |
| import org.eclipse.ptp.internal.rdt.sync.git.core.CommandRunner.CommandResults; |
| import org.eclipse.ptp.internal.rdt.sync.git.core.messages.Messages; |
| import org.eclipse.ptp.rdt.sync.core.AbstractSyncFileFilter; |
| import org.eclipse.ptp.rdt.sync.core.RecursiveSubMonitor; |
| import org.eclipse.ptp.rdt.sync.core.RemoteLocation; |
| import org.eclipse.ptp.rdt.sync.core.SyncFlag; |
| import org.eclipse.ptp.rdt.sync.core.SyncManager; |
| import org.eclipse.ptp.rdt.sync.core.exceptions.MissingConnectionException; |
| import org.eclipse.ptp.rdt.sync.core.exceptions.RemoteSyncException; |
| import org.eclipse.ptp.rdt.sync.core.exceptions.RemoteSyncMergeConflictException; |
| import org.eclipse.ptp.rdt.sync.core.services.AbstractSynchronizeService; |
| import org.eclipse.ptp.rdt.sync.core.services.ISynchronizeServiceDescriptor; |
| |
| /** |
| * A Git-based synchronization service for synchronized projects |
| */ |
| public class GitSyncService extends AbstractSynchronizeService { |
| public static final String gitDir = ".ptp-sync"; //$NON-NLS-1$ |
| // Name of file placed in empty directories to force sync'ing of those directories. |
| public static final String emptyDirectoryFileName = ".ptp-sync-folder"; //$NON-NLS-1$ |
| public static final String commitMessage = Messages.GitSyncService_0; |
| public static final String remotePushBranch = "ptp-push"; //$NON-NLS-1$ |
| |
| // Implement storage of local JGit repositories and Git repositories. |
| private static final Map<IPath, JGitRepo> localDirectoryToJGitRepoMap = new HashMap<IPath, JGitRepo>(); |
| private static final Map<RemoteLocation, GitRepo> remoteLocationToGitRepoMap = new HashMap<RemoteLocation, GitRepo>(); |
| |
| // Variables for managing sync threads |
| private static final ReentrantLock syncLock = new ReentrantLock(); |
| private static final ConcurrentMap<ProjectAndRemoteLocationPair, AtomicLong> syncLRPending = |
| new ConcurrentHashMap<ProjectAndRemoteLocationPair, AtomicLong>(); |
| private static final ConcurrentMap<ProjectAndRemoteLocationPair, AtomicLong> syncRLPending = |
| new ConcurrentHashMap<ProjectAndRemoteLocationPair, AtomicLong>(); |
| |
| // Entry indicates that the remote location has a clean (up-to-date) file filter for the project |
| private static final Set<LocalAndRemoteLocationPair> cleanFileFilterMap = new HashSet<LocalAndRemoteLocationPair>(); |
| // Entry indicates that the remote location contains the most recently committed local changes |
| private static final Set<LocalAndRemoteLocationPair> localChangesPushed = new HashSet<LocalAndRemoteLocationPair>(); |
| |
| // Boilerplate class for IPath and RemoteLocation Pair |
| private class LocalAndRemoteLocationPair { |
| IPath localDir; |
| RemoteLocation remoteLoc; |
| |
| /** |
| * Create new pair |
| * @param ld |
| * local directory |
| * @param rl |
| * remote location |
| */ |
| public LocalAndRemoteLocationPair(IPath ld, RemoteLocation rl) { |
| localDir = ld; |
| remoteLoc = rl; |
| } |
| |
| /** |
| * Get the local directory |
| * @return directory |
| */ |
| public IPath getLocal() { |
| return localDir; |
| } |
| |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + getOuterType().hashCode(); |
| result = prime * result |
| + ((localDir == null) ? 0 : localDir.hashCode()); |
| result = prime * result |
| + ((remoteLoc == null) ? 0 : remoteLoc.hashCode()); |
| return result; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (obj == null) { |
| return false; |
| } |
| if (getClass() != obj.getClass()) { |
| return false; |
| } |
| LocalAndRemoteLocationPair other = (LocalAndRemoteLocationPair) obj; |
| if (!getOuterType().equals(other.getOuterType())) { |
| return false; |
| } |
| if (localDir == null) { |
| if (other.localDir != null) { |
| return false; |
| } |
| } else if (!localDir.equals(other.localDir)) { |
| return false; |
| } |
| if (remoteLoc == null) { |
| if (other.remoteLoc != null) { |
| return false; |
| } |
| } else if (!remoteLoc.equals(other.remoteLoc)) { |
| return false; |
| } |
| return true; |
| } |
| |
| private GitSyncService getOuterType() { |
| return GitSyncService.this; |
| } |
| } |
| |
| // Boilerplate class for IProject and RemoteLocation Pair |
| private class ProjectAndRemoteLocationPair { |
| IProject project; |
| RemoteLocation remoteLoc; |
| |
| /** |
| * Create new pair |
| * @param p |
| * project |
| * @param rl |
| * remote location |
| */ |
| public ProjectAndRemoteLocationPair(IProject p, RemoteLocation rl) { |
| project = p; |
| remoteLoc = rl; |
| } |
| |
| @Override |
| public int hashCode() { |
| final int prime = 31; |
| int result = 1; |
| result = prime * result + getOuterType().hashCode(); |
| result = prime * result |
| + ((project == null) ? 0 : project.hashCode()); |
| result = prime * result |
| + ((remoteLoc == null) ? 0 : remoteLoc.hashCode()); |
| return result; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (this == obj) { |
| return true; |
| } |
| if (obj == null) { |
| return false; |
| } |
| if (getClass() != obj.getClass()) { |
| return false; |
| } |
| ProjectAndRemoteLocationPair other = (ProjectAndRemoteLocationPair) obj; |
| if (!getOuterType().equals(other.getOuterType())) { |
| return false; |
| } |
| if (project == null) { |
| if (other.project != null) { |
| return false; |
| } |
| } else if (!project.equals(other.project)) { |
| return false; |
| } |
| if (remoteLoc == null) { |
| if (other.remoteLoc != null) { |
| return false; |
| } |
| } else if (!remoteLoc.equals(other.remoteLoc)) { |
| return false; |
| } |
| return true; |
| } |
| |
| private GitSyncService getOuterType() { |
| return GitSyncService.this; |
| } |
| } |
| |
| /** |
| * Get JGit repository instance for the given project, creating it if necessary. |
| * @param project - cannot be null |
| * @param monitor |
| * |
| * @return JGit repository instance - never null |
| * @throws RemoteSyncException |
| * on problems creating the repository |
| */ |
| static JGitRepo getLocalJGitRepo(IProject project, IProgressMonitor monitor) throws RemoteSyncException { |
| IPath localDir = project.getLocation(); |
| if (localDir == null) { |
| throw new RemoteSyncException(Messages.GitSyncService_17 + project.getName()); |
| } |
| JGitRepo repo = localDirectoryToJGitRepoMap.get(localDir); |
| try { |
| if (repo == null) { |
| try { |
| repo = new JGitRepo(localDir, monitor); |
| localDirectoryToJGitRepoMap.put(localDir, repo); |
| setRepoFilesAsDerived(project); |
| } catch (GitAPIException e) { |
| throw new RemoteSyncException(e); |
| } catch (IOException e) { |
| throw new RemoteSyncException(e); |
| } |
| } |
| } finally { |
| if (monitor != null) { |
| monitor.done(); |
| } |
| } |
| return repo; |
| } |
| |
| /** |
| * Get Git repository instance for given remote location, creating it if necessary. |
| * |
| * @param rl |
| * remote location - cannot be null |
| * @param monitor |
| * |
| * @return Git repo instance - is null only if connection could not be resolved. |
| * @throws RemoteSyncException |
| * on problems creating the repository |
| */ |
| static GitRepo getGitRepo(RemoteLocation rl, IProgressMonitor monitor) throws RemoteSyncException { |
| if (rl == null) { |
| throw new NullPointerException(); |
| } |
| GitRepo repo = remoteLocationToGitRepoMap.get(rl); |
| try { |
| if (repo == null) { |
| RecursiveSubMonitor subMon = RecursiveSubMonitor.convert(monitor, 100); |
| subMon.subTask(Messages.GitSyncService_1); |
| try { |
| subMon.subTask(Messages.GitSyncService_2); |
| repo = new GitRepo(rl, subMon.newChild(90)); |
| remoteLocationToGitRepoMap.put(rl, repo); |
| } catch (MissingConnectionException e) { |
| return null; |
| } |
| } |
| } finally { |
| if (monitor != null) { |
| monitor.done(); |
| } |
| } |
| return repo; |
| } |
| |
| private static boolean consCalled = false; |
| /** |
| * Create a new instance of the GitSyncService |
| * @param descriptor |
| * service descriptor |
| */ |
| public GitSyncService(ISynchronizeServiceDescriptor descriptor) { |
| super(descriptor); |
| // Constructor for each sync service should only be called once by design of synchronized projects |
| // See bug 410106 |
| assert(!consCalled) : Messages.GitSyncService_3; |
| consCalled = true; |
| } |
| |
| /* |
| * (non-Javadoc) |
| * @see org.eclipse.ptp.rdt.sync.core.services.ISynchronizeService#checkout(org.eclipse.core.resources.IProject, |
| * org.eclipse.core.runtime.IPath[]) |
| */ |
| @Override |
| public void checkout(IProject project, IPath[] paths) throws RemoteSyncException { |
| JGitRepo repo = getLocalJGitRepo(project, null); |
| if (repo != null) { |
| try { |
| repo.checkout(paths); |
| doRefresh(project); |
| } catch (GitAPIException e) { |
| throw new RemoteSyncException(e); |
| } |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * @see org.eclipse.ptp.rdt.sync.core.services.ISynchronizeService#checkoutRemoteCopy(org.eclipse.core.resources.IProject, |
| * org.eclipse.core.runtime.IPath[]) |
| */ |
| @Override |
| public void checkoutRemoteCopy(IProject project, IPath[] paths) throws RemoteSyncException { |
| JGitRepo repo = getLocalJGitRepo(project, null); |
| if (repo != null) { |
| try { |
| repo.checkoutRemoteCopy(paths); |
| doRefresh(project); |
| } catch (GitAPIException e) { |
| throw new RemoteSyncException(e); |
| } |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.eclipse.ptp.rdt.sync.core.serviceproviders.ISyncServiceProvider#close( |
| * org.eclipse.core.resources.IProject) |
| */ |
| @Override |
| public synchronized void close(IProject project) throws RemoteSyncException { |
| JGitRepo repo = getLocalJGitRepo(project, null); |
| if (repo != null) { |
| repo.close(); |
| } |
| |
| Iterator<Map.Entry<IPath, JGitRepo>> it = localDirectoryToJGitRepoMap.entrySet().iterator(); |
| while (it.hasNext()) { |
| Map.Entry<IPath, JGitRepo> entry = it.next(); |
| if (entry.getValue() == repo) { |
| it.remove(); |
| } |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * @see org.eclipse.ptp.rdt.sync.core.services.ISynchronizeService#getMergeConflictFiles( |
| * org.eclipse.core.resources.IProject) |
| */ |
| @Override |
| public Set<IPath> getMergeConflictFiles(IProject project) throws RemoteSyncException { |
| JGitRepo repo = getLocalJGitRepo(project, null); |
| if (repo == null) { |
| return new HashSet<IPath>(); |
| } else { |
| try { |
| return repo.getMergeConflictFiles(); |
| } catch (GitAPIException e) { |
| throw new RemoteSyncException(e); |
| } catch (IOException e) { |
| throw new RemoteSyncException(e); |
| } |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * @see org.eclipse.ptp.rdt.sync.core.services.ISynchronizeService#getMergeConflictParts(org.eclipse.core.resources.IProject, |
| * org.eclipse.core.resources.IFile) |
| */ |
| @Override |
| public String[] getMergeConflictParts(IProject project, IFile file) throws RemoteSyncException { |
| JGitRepo repo = getLocalJGitRepo(project, null); |
| if (repo == null) { |
| return null; |
| } else { |
| try { |
| return repo.getMergeConflictParts(file.getProjectRelativePath()); |
| } catch (GitAPIException e) { |
| throw new RemoteSyncException(e); |
| } catch (IOException e) { |
| throw new RemoteSyncException(e); |
| } |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * @see org.eclipse.ptp.rdt.sync.core.services.ISynchronizeService#setMergeAsResolved(org.eclipse.core.resources.IProject, |
| * org.eclipse.core.runtime.IPath[]) |
| */ |
| @Override |
| public void setMergeAsResolved(IProject project, IPath[] paths) throws RemoteSyncException { |
| JGitRepo repo = getLocalJGitRepo(project, null); |
| if (repo != null) { |
| try { |
| repo.setMergeAsResolved(paths); |
| } catch (GitAPIException e) { |
| throw new RemoteSyncException(e); |
| } |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * @see org.eclipse.ptp.rdt.sync.core.services.ISynchronizeService#synchronize(org.eclipse.core.resources.IProject, |
| * org.eclipse.ptp.rdt.sync.core.RemoteLocation, org.eclipse.core.resources.IResourceDelta, |
| * org.eclipse.core.runtime.IProgressMonitor, java.util.EnumSet) |
| */ |
| @Override |
| public void synchronize(final IProject project, RemoteLocation rl, IResourceDelta delta, IProgressMonitor monitor, |
| Set<SyncFlag> syncFlags) throws CoreException { |
| if (project == null || rl == null) { |
| throw new NullPointerException(); |
| } |
| RemoteLocation remoteLoc = new RemoteLocation(rl); |
| RecursiveSubMonitor subMon = RecursiveSubMonitor.convert(monitor, 100); |
| |
| try { |
| /* |
| * A synchronize with SyncFlag.BOTH guarantees that both directories are in sync. |
| * |
| * More precise: it guarantees that all changes written to disk at the moment of the call are guaranteed to be |
| * synchronized between both directories. No guarantees are given for changes occurring during the synchronize call. |
| * |
| * To satisfy this guarantee, this call needs to make sure that both the current delta and all outstanding sync |
| * requests finish before this call returns. |
| * |
| * Example: Why sync if current delta is empty? The RemoteMakeBuilder forces a sync before and after building. In some |
| * cases, we want to ensure repos are synchronized regardless of the passed delta, which can be set to null. |
| */ |
| |
| ProjectAndRemoteLocationPair syncTarget = new ProjectAndRemoteLocationPair(project, remoteLoc); |
| Boolean syncLR = syncFlags.contains(SyncFlag.SYNC_LR); |
| Boolean syncRL = syncFlags.contains(SyncFlag.SYNC_RL); |
| Set<SyncFlag> modifiedSyncFlags = new HashSet<SyncFlag>(syncFlags); |
| |
| // Do not sync LR (local-to-remote) if another thread is already waiting to do it. |
| if (syncLR) { |
| AtomicLong threadCount = syncLRPending.putIfAbsent(syncTarget, new AtomicLong(1)); |
| if (threadCount != null) { |
| if (threadCount.get() > 0) { |
| syncLR = false; |
| modifiedSyncFlags.remove(SyncFlag.SYNC_LR); |
| } else { |
| threadCount.incrementAndGet(); |
| } |
| } |
| } |
| |
| // Do not sync RL (remote-to-local) if another thread is already waiting to do it. |
| if (syncRL) { |
| AtomicLong threadCount = syncRLPending.putIfAbsent(syncTarget, new AtomicLong(1)); |
| if (threadCount != null) { |
| if (threadCount.get() > 0) { |
| syncRL = false; |
| modifiedSyncFlags.remove(SyncFlag.SYNC_RL); |
| } else { |
| threadCount.incrementAndGet(); |
| } |
| } |
| } |
| |
| // Return if we have nothing to do. |
| if (!(syncLR || syncRL)) { |
| return; |
| } |
| |
| // lock syncLock. interruptible by progress monitor |
| try { |
| while (!syncLock.tryLock(50, TimeUnit.MILLISECONDS)) { |
| if (subMon.isCanceled()) { |
| throw new CoreException(new Status(IStatus.CANCEL, Activator.PLUGIN_ID, Messages.GitSyncService_4)); |
| } |
| } |
| } catch (InterruptedException e1) { |
| throw new CoreException(new Status(IStatus.CANCEL, Activator.PLUGIN_ID, Messages.GitSyncService_5)); |
| } finally { |
| if (syncLR) { |
| AtomicLong LRPending = syncLRPending.get(syncTarget); |
| assert(LRPending != null) : Messages.GitSyncService_20; |
| LRPending.decrementAndGet(); |
| } |
| if (syncRL) { |
| AtomicLong RLPending = syncRLPending.get(syncTarget); |
| assert(RLPending != null) : Messages.GitSyncService_21; |
| RLPending.decrementAndGet(); |
| } |
| } |
| |
| try { |
| subMon.subTask(Messages.GitSyncService_9); |
| doSync(project, remoteLoc, modifiedSyncFlags, subMon.newChild(95)); |
| } catch (RemoteSyncMergeConflictException e) { |
| subMon.subTask(Messages.GitSyncService_10); |
| // Refresh after merge conflict since conflicted files are altered with markup. |
| doRefresh(project); |
| throw e; |
| } finally { |
| syncLock.unlock(); |
| } |
| |
| // Sync successful - re-enable error messages. This is really UI code, but there is no way at the moment to notify UI |
| // of a successful sync. |
| SyncManager.setShowErrors(project, true); |
| |
| // Refresh after sync to display changes |
| subMon.subTask(Messages.GitSyncService_10); |
| doRefresh(project); |
| } finally { |
| if (monitor != null) { |
| monitor.done(); |
| } |
| } |
| } |
| |
| /** |
| * Synchronize the given project to the given remote. |
| * The sync strategy follows these three high-level steps: |
| * 1) Commit local changes. |
| * 2) If sync RL is requested, commit and fetch remote changes and then merge them locally. |
| * 3) If sync LR is requested, push local changes to remote and merge. Only fast-forward merging is allowed to prevent remote |
| * merge conflicts. On failure, do a sync RL, even if not requested, to capture remote changes, and repeat sync LR. It is |
| * important that merge conflicts happen locally, so they can be handled in Eclipse with JGit. If a merge conflict occurs, |
| * sync'ing is halted (no sync LR is done) and only re-enabled once the conflicts are resolved. |
| * |
| * @param project |
| * the local project |
| * @param remoteLoc |
| * the remote location |
| * @param syncFlags |
| * flags to modify sync behavior |
| * @param monitor |
| * @throws RemoteSyncException |
| * for various problems sync'ing. All exceptions are wrapped in a RemoteSyncException and thrown, so that clients |
| * can always detect when a sync fails and why. |
| */ |
| private void doSync(IProject project, RemoteLocation remoteLoc, Set<SyncFlag> syncFlags, IProgressMonitor monitor) |
| throws RemoteSyncException { |
| boolean syncLR = syncFlags.contains(SyncFlag.SYNC_LR); |
| boolean syncRL = syncFlags.contains(SyncFlag.SYNC_RL); |
| |
| RecursiveSubMonitor subMon = RecursiveSubMonitor.convert(monitor, 100); |
| try { |
| subMon.subTask(Messages.GitSyncService_6); |
| JGitRepo localRepo = getLocalJGitRepo(project, subMon.newChild(5)); |
| |
| if (localRepo.inUnresolvedMergeState()) { |
| throw new RemoteSyncMergeConflictException(Messages.GitSyncService_8); |
| } |
| |
| LocalAndRemoteLocationPair lrpair = new LocalAndRemoteLocationPair(localRepo.getDirectory(), remoteLoc); |
| |
| // Commit local changes |
| subMon.subTask(Messages.GitSyncService_12); |
| if (localRepo.commit(subMon.newChild(5))) { |
| // New changes - mark all local/remote pairs with current local as needing to be updated. |
| Iterator<LocalAndRemoteLocationPair> it = localChangesPushed.iterator(); |
| while (it.hasNext()) { |
| LocalAndRemoteLocationPair lrp = it.next(); |
| if (lrp.getLocal().equals(localRepo.getDirectory())) { |
| it.remove(); |
| } |
| } |
| } |
| |
| // Return early if local changes have already been pushed and remote-to-local sync was not requested. |
| if ((!syncLR || localChangesPushed.contains(lrpair)) && (!syncRL)) { |
| return; |
| } |
| |
| // Get remote repository. creating it if necessary |
| subMon.subTask(Messages.GitSyncService_7); |
| GitRepo remoteRepo = getGitRepo(remoteLoc, subMon.newChild(10)); |
| |
| // Unresolved connection - abort |
| if (remoteRepo == null) { |
| return; |
| } |
| |
| // Update remote file filter |
| if (!cleanFileFilterMap.contains(lrpair)) { |
| subMon.subTask(Messages.GitSyncService_11); |
| remoteRepo.uploadFilter(localRepo, subMon.newChild(10)); |
| cleanFileFilterMap.add(lrpair); |
| } |
| |
| // Sync remote to local |
| if (syncRL) { |
| subMon.subTask(Messages.GitSyncService_24); |
| doSyncRL(localRepo, remoteRepo, subMon.newChild(30)); |
| } |
| |
| // Sync local to remote |
| if (syncLR && (!localChangesPushed.contains(lrpair))) { |
| subMon.subTask(Messages.GitSyncService_25); |
| CommandResults results = doSyncLR(localRepo, remoteRepo, subMon.newChild(30)); |
| boolean success = (results.getExitCode() == 0); |
| // If the remote merge fails and sync RL was not done, then the failure *most likely* occurred because files |
| // changed on remote. So we do a sync RL to capture changes and then sync LR again. |
| if (!success && !syncRL) { |
| subMon.subTask(Messages.GitSyncService_26); |
| doSyncRL(localRepo, remoteRepo, subMon.newChild(30)); |
| subMon.subTask(Messages.GitSyncService_27); |
| results = doSyncLR(localRepo, remoteRepo, subMon.newChild(10)); |
| success = (results.getExitCode() == 0); |
| } |
| if (success) { |
| localChangesPushed.add(lrpair); |
| } else { |
| // This should never happen since sync RL was done prior to sync LR. Only remote changes during the sync or |
| // an unanticipated problem could lead to here. |
| throw new RemoteSyncException(Messages.GitSyncService_23 + results.getStderr()); |
| } |
| } |
| } catch (final IOException e) { |
| throw new RemoteSyncException(e); |
| } catch (GitAPIException e) { |
| throw new RemoteSyncException(e); |
| } catch (MissingConnectionException e) { |
| // nothing to do |
| } finally { |
| if (monitor != null) { |
| monitor.done(); |
| } |
| } |
| } |
| |
| // Returns whether sync succeeds (true) or fails (false) in an expected way. Failure can occur normally if remote files |
| // were altered, and thus the remote merge cannot be completed. |
| private CommandResults doSyncLR(JGitRepo localRepo, GitRepo remoteRepo, IProgressMonitor monitor) throws RemoteSyncException, |
| MissingConnectionException { |
| RecursiveSubMonitor subMon = RecursiveSubMonitor.convert(monitor, 30); |
| |
| try { |
| if (localRepo.getGit().branchList().call().size() > 0) { // check whether master was already created |
| subMon.subTask(Messages.GitSyncService_18); |
| localRepo.push(remoteRepo, subMon.newChild(20)); |
| subMon.subTask(Messages.GitSyncService_28); |
| return remoteRepo.merge(subMon.newChild(10)); |
| } |
| return new CommandResults(); // Successful result with no output |
| } catch (TransportException e) { |
| throw new RemoteSyncException(e); |
| } catch (GitAPIException e) { |
| throw new RemoteSyncException(e); |
| } finally { |
| if (monitor != null) { |
| monitor.done(); |
| } |
| } |
| } |
| |
| private void doSyncRL(JGitRepo localRepo, GitRepo remoteRepo, IProgressMonitor monitor) throws RemoteSyncException, |
| MissingConnectionException { |
| RecursiveSubMonitor subMon = RecursiveSubMonitor.convert(monitor, 90); |
| |
| try { |
| // Commit remote changes |
| subMon.subTask(Messages.GitSyncService_13); |
| remoteRepo.commitRemoteFiles(subMon.newChild(40)); |
| |
| // Download remote changes |
| subMon.subTask(Messages.GitSyncService_14); |
| try { |
| localRepo.fetch(remoteRepo, subMon.newChild(40)); |
| } catch (TransportException e) { |
| // Fetch can fail simply because the remote branch isn't set up yet. So just return in that case and throw an |
| // exception otherwise. |
| // Even for the first case, however, a proceeding merge will fail (remote ref is not created), so be sure to |
| // always abort and not attempt a merge. |
| if (e.getMessage().startsWith("Remote does not have ")) { //$NON-NLS-1$ |
| return; |
| } else { |
| throw new RemoteSyncException(e); |
| } |
| } |
| |
| // Merge remote changes into local repository |
| subMon.subTask(Messages.GitSyncService_15); |
| try { |
| org.eclipse.jgit.api.MergeResult mergeResult = localRepo.merge(subMon.newChild(10)); |
| if (mergeResult.getFailingPaths() != null) { |
| String message = Messages.GitSyncService_16; |
| for (String s : mergeResult.getFailingPaths().keySet()) { |
| message += System.getProperty("line.separator") + s; //$NON-NLS-1$ |
| } |
| throw new RemoteSyncException(message); |
| } |
| if (localRepo.inUnresolvedMergeState()) { |
| throw new RemoteSyncMergeConflictException(Messages.GitSyncService_8); |
| // Even if we later decide not to throw an exception, it is important not to proceed after a merge conflict. |
| // return; |
| } |
| } catch (GitAPIException e) { |
| throw new RemoteSyncException(e); |
| } catch (IOException e) { |
| throw new RemoteSyncException(e); |
| } |
| } finally { |
| if (monitor != null) { |
| monitor.done(); |
| } |
| } |
| } |
| |
| /* |
| * (non-Javadoc) |
| * @see org.eclipse.ptp.rdt.sync.core.services.ISynchronizeService#getSyncFileFilter(org.eclipse.core.resources.IProject) |
| */ |
| @Override |
| public AbstractSyncFileFilter getSyncFileFilter(IProject project) throws RemoteSyncException { |
| return getLocalJGitRepo(project, null).getFilter(); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * @see org.eclipse.ptp.rdt.sync.core.services.ISynchronizeService#setSyncFileFilter(org.eclipse.core.resources.IProject, |
| * org.eclipse.ptp.rdt.sync.core.AbstractSyncFileFilter) |
| */ |
| @Override |
| public void setSyncFileFilter(IProject project, AbstractSyncFileFilter filter) throws RemoteSyncException { |
| Iterator<LocalAndRemoteLocationPair> it = cleanFileFilterMap.iterator(); |
| IPath localDir = project.getLocation(); |
| if (localDir == null) { |
| throw new RemoteSyncException(Messages.GitSyncService_17 + project.getName()); |
| } |
| while (it.hasNext()) { |
| LocalAndRemoteLocationPair lp = it.next(); |
| if (lp.getLocal().equals(localDir)) { |
| it.remove(); |
| } |
| } |
| JGitRepo localRepo = getLocalJGitRepo(project, null); |
| localRepo.setFilter(filter); |
| } |
| |
| // Refresh the workspace in a separate thread |
| // Bug 374409 - this prevents deadlock caused by locking both the sync lock and the workspace lock. |
| private static Thread doRefresh(final IProject project) { |
| Thread refreshWorkspaceThread = new Thread(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| project.refreshLocal(IResource.DEPTH_INFINITE, null); |
| } catch (CoreException e) { |
| Activator.log(Messages.JGitRepo_16, e); |
| } |
| } |
| }, "Refresh workspace thread"); //$NON-NLS-1$ |
| refreshWorkspaceThread.start(); |
| return refreshWorkspaceThread; |
| } |
| |
| // Set Git repository files as derived. |
| // This prevents user-level operations, such as searching, from considering the repository directory. |
| private static void setRepoFilesAsDerived(final IProject project) { |
| // First refresh project so that files appear |
| final Thread refreshThread = doRefresh(project); |
| |
| // Set derived only after refresh completes |
| Thread setDerivedThread = new Thread(new Runnable() { |
| @Override |
| public void run() { |
| try { |
| refreshThread.join(); |
| project.getFolder(GitSyncService.gitDir).setDerived(true, null); |
| } catch (InterruptedException e) { |
| Activator.log(e); |
| } catch (CoreException e) { |
| Activator.log(e); |
| } |
| } |
| }, "Set repository as derived thread"); //$NON-NLS-1$ |
| setDerivedThread.start(); |
| } |
| } |