| /******************************************************************************* |
| * Copyright (c) 2000, 2011 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Matt McCutchen <hashproduct+eclipse@gmail.com> - Bug 181546 [Sync Info] Eclipse writes Entries-less metadata in recreated pruned dir |
| *******************************************************************************/ |
| package org.eclipse.team.internal.ccvs.core.resources; |
| |
| import java.util.*; |
| |
| import org.eclipse.core.resources.*; |
| import org.eclipse.core.runtime.*; |
| import org.eclipse.core.runtime.jobs.*; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.team.core.TeamException; |
| import org.eclipse.team.internal.ccvs.core.*; |
| import org.eclipse.team.internal.ccvs.core.syncinfo.*; |
| import org.eclipse.team.internal.ccvs.core.syncinfo.ReentrantLock.CVSThreadInfo; |
| import org.eclipse.team.internal.ccvs.core.util.*; |
| import org.eclipse.team.internal.core.subscribers.BatchingLock.IFlushOperation; |
| import org.eclipse.team.internal.core.subscribers.BatchingLock.ThreadInfo; |
| import org.osgi.framework.Bundle; |
| |
| /** |
| * A synchronizer is responsible for managing synchronization information for local |
| * CVS resources. |
| * |
| * This class is thread safe but only allows one thread to modify the cache at a time. It |
| * doesn't support fine grain locking on a resource basis. Lock ordering between the workspace |
| * lock and the synchronizer lock is guaranteed to be deterministic. That is, the workspace |
| * lock is *always* acquired before the synchronizer lock. This protects against possible |
| * deadlock cases where the synchronizer lock is acquired before a workspace lock. |
| * |
| * Special processing has been added for linked folders and their childen so |
| * that their CVS meta files are never read or written. |
| * |
| * IMPORTANT NOTICE: It is the responsibility of the clients of EclipseSynchronizer |
| * to ensure that they have wrapped operations that may modify the workspace in |
| * an IWorkspaceRunnable. If this is not done, deltas may fore at inopertune times |
| * and corrupt the sync info. The wrapping could be done within the synchronizer |
| * itself but would require the creation of an inner class for each case that requires |
| * it. |
| * |
| * @see ResourceSyncInfo |
| * @see FolderSyncInfo |
| */ |
| public class EclipseSynchronizer implements IFlushOperation { |
| private static final String IS_DIRTY_INDICATOR = SyncInfoCache.IS_DIRTY_INDICATOR; |
| private static final String NOT_DIRTY_INDICATOR = SyncInfoCache.NOT_DIRTY_INDICATOR; |
| private static final String RECOMPUTE_INDICATOR = SyncInfoCache.RECOMPUTE_INDICATOR; |
| |
| // the cvs eclipse synchronizer is a singleton |
| private static EclipseSynchronizer instance; |
| |
| // track resources that have changed in a given operation |
| private ILock lock = Job.getJobManager().newLock(); |
| private ReentrantLock resourceLock = new ReentrantLock(); |
| |
| private SynchronizerSyncInfoCache synchronizerCache = new SynchronizerSyncInfoCache(); |
| private SessionPropertySyncInfoCache sessionPropertyCache = new SessionPropertySyncInfoCache(synchronizerCache); |
| |
| /* |
| * Package private constructor to allow specialized subclass for handling folder deletions |
| */ |
| EclipseSynchronizer() { |
| } |
| |
| /** |
| * Returns the singleton instance of the synchronizer. |
| */ |
| public static EclipseSynchronizer getInstance() { |
| if(instance==null) { |
| instance = new EclipseSynchronizer(); |
| } |
| return instance; |
| } |
| |
| public SyncInfoCache getSyncInfoCacheFor(IResource resource) { |
| if (resource.exists() && resource.isLocal(IResource.DEPTH_ZERO)) { |
| return sessionPropertyCache; |
| } else { |
| return synchronizerCache; |
| } |
| } |
| |
| private boolean isValid(IResource resource) { |
| return resource.exists() || synchronizerCache.isPhantom(resource); |
| } |
| |
| /** |
| * Sets the folder sync info for the specified folder. |
| * The folder must exist and must not be the workspace root. |
| * |
| * @param folder the folder |
| * @param info the folder sync info, must not be null |
| * @see #getFolderSync |
| * @see #deleteFolderSync |
| */ |
| public void setFolderSync(IContainer folder, FolderSyncInfo info) throws CVSException { |
| Assert.isNotNull(info); // enforce the use of deleteFolderSync |
| // ignore folder sync on the root (i.e. CVSROOT/config/TopLevelAdmin=yes but we just ignore it) |
| if (folder.getType() == IResource.ROOT) return; |
| if (!isValid(folder)) { |
| // This means that the folder doesn't exist and is not a phantom |
| // Allow the set if the parent is a CVS folder since |
| // this can occur when creating phantom folders |
| if (getFolderSync(folder.getParent()) == null) { |
| IStatus status = new CVSStatus(IStatus.ERROR, TeamException.UNABLE, |
| NLS.bind(CVSMessages.EclipseSynchronizer_ErrorSettingFolderSync, new String[] { folder.getFullPath().toString() }),folder); |
| throw new CVSException(status); |
| } |
| } |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(folder, null); |
| try { |
| beginOperation(); |
| // get the old info |
| FolderSyncInfo oldInfo = getFolderSync(folder); |
| // set folder sync and notify |
| getSyncInfoCacheFor(folder).setCachedFolderSync(folder, info, true); |
| // if the sync info changed from null, we may need to adjust the ancestors |
| if (oldInfo == null) { |
| adjustDirtyStateRecursively(folder, RECOMPUTE_INDICATOR); |
| } |
| folderChanged(folder); |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| |
| /** |
| * Gets the folder sync info for the specified folder. |
| * |
| * @param folder the folder |
| * @return the folder sync info associated with the folder, or null if none. |
| * @see #setFolderSync |
| * @see #deleteFolderSync |
| */ |
| public FolderSyncInfo getFolderSync(IContainer folder) throws CVSException { |
| if (folder.getType() == IResource.ROOT || !isValid(folder)) return null; |
| // Do a check outside the lock for any folder sync info |
| FolderSyncInfo info = getSyncInfoCacheFor(folder).getCachedFolderSync(folder, false /* not thread safe */); |
| if (info != null) |
| return info; |
| try { |
| beginOperation(); |
| cacheFolderSync(folder); |
| return getSyncInfoCacheFor(folder).getCachedFolderSync(folder, true /* thread safe */); |
| } finally { |
| endOperation(); |
| } |
| } |
| |
| /** |
| * Deletes the folder sync for the specified folder and the resource sync |
| * for all of its children. Does not recurse. |
| * |
| * @param folder the folder |
| * @see #getFolderSync |
| * @see #setFolderSync |
| */ |
| public void deleteFolderSync(IContainer folder) throws CVSException { |
| if (folder.getType() == IResource.ROOT || !isValid(folder)) return; |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(folder, null); |
| try { |
| beginOperation(); |
| // iterate over all children with sync info and prepare notifications |
| // this is done first since deleting the folder sync may remove a phantom |
| cacheResourceSyncForChildren(folder, true /* can modify workspace */); |
| IResource[] children = folder.members(true); |
| for (IResource resource : children) { |
| resourceChanged(resource); |
| // delete resource sync for all children |
| getSyncInfoCacheFor(resource).setCachedSyncBytes(resource, null, true); |
| } |
| // delete folder sync |
| getSyncInfoCacheFor(folder).setCachedFolderSync(folder, null, true); |
| folderChanged(folder); |
| } catch (CoreException e) { |
| throw CVSException.wrapException(e); |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| |
| private void folderChanged(IContainer folder) { |
| resourceLock.folderChanged(folder); |
| } |
| |
| private void resourceChanged(IResource resource) { |
| resourceLock.resourceChanged(resource); |
| } |
| |
| /** |
| * Sets the resource sync info for the specified resource. |
| * The parent folder must exist and must not be the workspace root. |
| * |
| * @param resource the resource |
| * @param info the resource sync info, must not be null |
| * @see #getResourceSync |
| * @see #deleteResourceSync |
| */ |
| public void setResourceSync(IResource resource, ResourceSyncInfo info) throws CVSException { |
| Assert.isNotNull(info); // enforce the use of deleteResourceSync |
| IContainer parent = resource.getParent(); |
| if (parent == null || parent.getType() == IResource.ROOT || !isValid(parent)) { |
| IStatus status = new CVSStatus(IStatus.ERROR, TeamException.UNABLE, |
| NLS.bind(CVSMessages.EclipseSynchronizer_ErrorSettingResourceSync, new String[] { resource.getFullPath().toString() }), resource); |
| throw new CVSException(status); |
| } |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(resource, null); |
| try { |
| beginOperation(); |
| // cache resource sync for siblings, set for self, then notify |
| cacheResourceSyncForChildren(parent, true /* can modify workspace */); |
| setCachedResourceSync(resource, info); |
| resourceChanged(resource); |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| |
| /** |
| * Gets the resource sync info for the specified folder. |
| * |
| * @param resource the resource |
| * @return the resource sync info associated with the resource, or null if none. |
| * @see #setResourceSync |
| * @see #deleteResourceSync |
| */ |
| public ResourceSyncInfo getResourceSync(IResource resource) throws CVSException { |
| byte[] info = getSyncBytes(resource); |
| if (info == null) return null; |
| return new ResourceSyncInfo(info); |
| } |
| |
| /** |
| * Gets the resource sync info for the specified folder. |
| * |
| * @param resource the resource |
| * @return the resource sync info associated with the resource, or null if none. |
| * @see #setResourceSync |
| * @see #deleteResourceSync |
| */ |
| public byte[] getSyncBytes(IResource resource) throws CVSException { |
| IContainer parent = resource.getParent(); |
| if (parent == null || parent.getType() == IResource.ROOT || !isValid(parent)) return null; |
| // Do a quick check outside the lock to see if there are sync butes for the resource. |
| byte[] info = getSyncInfoCacheFor(resource).getCachedSyncBytes(resource, false /* not thread safe */); |
| if (info != null) |
| return info; |
| try { |
| beginOperation(); |
| // cache resource sync for siblings, then return for self |
| try { |
| cacheResourceSyncForChildren(parent, false /* cannot modify workspace */); |
| } catch (CVSException e) { |
| if (isCannotModifySynchronizer(e) || isResourceNotFound(e)) { |
| // We will resort to loading the sync info for the requested resource from disk |
| byte[] bytes = getSyncBytesFromDisk(resource); |
| if (!resource.exists() && bytes != null && !ResourceSyncInfo.isDeletion(bytes)) { |
| bytes = ResourceSyncInfo.convertToDeletion(bytes); |
| } |
| return bytes; |
| } else { |
| throw e; |
| } |
| } |
| return getCachedSyncBytes(resource); |
| } finally { |
| endOperation(); |
| } |
| } |
| |
| /** |
| * Sets the resource sync info for the specified resource. |
| * The parent folder must exist and must not be the workspace root. |
| * |
| * @param resource the resource |
| * @param info the resource sync info, must not be null |
| * @see #getResourceSync |
| * @see #deleteResourceSync |
| */ |
| public void setSyncBytes(IResource resource, byte[] syncBytes) throws CVSException { |
| Assert.isNotNull(syncBytes); // enforce the use of deleteResourceSync |
| IContainer parent = resource.getParent(); |
| if (parent == null || parent.getType() == IResource.ROOT || !isValid(parent)) { |
| IStatus status = new CVSStatus(IStatus.ERROR, TeamException.UNABLE, |
| NLS.bind(CVSMessages.EclipseSynchronizer_ErrorSettingResourceSync, new String[] { resource.getFullPath().toString() }),resource); |
| throw new CVSException(status); |
| } |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(resource, null); |
| try { |
| beginOperation(); |
| // cache resource sync for siblings, set for self, then notify |
| cacheResourceSyncForChildren(parent, true /* can modify workspace */); |
| setCachedSyncBytes(resource, syncBytes); |
| resourceChanged(resource); |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| |
| /** |
| * Deletes the resource sync info for the specified resource, if it exists. |
| * |
| * @param resource the resource |
| * @see #getResourceSync |
| * @see #setResourceSync |
| */ |
| public void deleteResourceSync(IResource resource) throws CVSException { |
| IContainer parent = resource.getParent(); |
| if (parent == null || parent.getType() == IResource.ROOT || !isValid(parent)) return; |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(resource, null); |
| try { |
| beginOperation(); |
| // cache resource sync for siblings, delete for self, then notify |
| cacheResourceSyncForChildren(parent, true /* can modify workspace */); |
| if (getCachedSyncBytes(resource) != null) { // avoid redundant notifications |
| setCachedSyncBytes(resource, null); |
| clearDirtyIndicator(resource); |
| resourceChanged(resource); |
| } |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| |
| /** |
| * @param resource |
| */ |
| private void clearDirtyIndicator(IResource resource) throws CVSException { |
| getSyncInfoCacheFor(resource).flushDirtyCache(resource); |
| adjustDirtyStateRecursively(resource.getParent(), RECOMPUTE_INDICATOR); |
| } |
| |
| /** |
| * Gets the array of ignore patterns for the specified folder. |
| * |
| * @param folder the folder |
| * @return the patterns, or an empty array if none |
| * @see #addIgnored |
| */ |
| public boolean isIgnored(IResource resource) throws CVSException { |
| if (resource.getType() == IResource.ROOT || |
| resource.getType() == IResource.PROJECT || |
| ! resource.exists()) { |
| return false; |
| } |
| IContainer parent = resource.getParent(); |
| FileNameMatcher matcher = sessionPropertyCache.getFolderIgnores(parent, false /* not thread safe */); |
| if (matcher == null) { |
| try { |
| beginOperation(); |
| matcher = cacheFolderIgnores(parent); |
| } finally { |
| endOperation(); |
| } |
| } |
| return matcher.match(resource.getName()); |
| } |
| |
| /** |
| * Adds a pattern to the set of ignores for the specified folder. |
| * |
| * @param folder the folder |
| * @param pattern the pattern |
| */ |
| public void addIgnored(IContainer folder, String pattern) throws CVSException { |
| if (folder.getType() == IResource.ROOT || ! folder.exists()) { |
| IStatus status = new CVSStatus(IStatus.ERROR, TeamException.UNABLE, |
| NLS.bind(CVSMessages.EclipseSynchronizer_ErrorSettingIgnorePattern, new String[] { folder.getFullPath().toString() }),folder); |
| throw new CVSException(status); |
| } |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(folder.getFile(new Path(SyncFileWriter.IGNORE_FILE)), null); |
| try { |
| beginOperation(); |
| String[] ignores = SyncFileWriter.readCVSIgnoreEntries(folder); |
| if (ignores != null) { |
| // verify that the pattern has not already been added |
| for (String ignore : ignores) { |
| if (ignore.equals(pattern)) { |
| return; |
| } |
| } |
| // add the pattern |
| String[] oldIgnores = ignores; |
| ignores = new String[oldIgnores.length + 1]; |
| System.arraycopy(oldIgnores, 0, ignores, 0, oldIgnores.length); |
| ignores[oldIgnores.length] = pattern; |
| } else { |
| ignores = new String[] { pattern }; |
| } |
| setCachedFolderIgnores(folder, ignores); |
| SyncFileWriter.writeCVSIgnoreEntries(folder, ignores); |
| // broadcast changes to unmanaged children - they are the only candidates for being ignored |
| List<IResource> possibleIgnores = new ArrayList<>(); |
| accumulateNonManagedChildren(folder, possibleIgnores); |
| ResourceStateChangeListeners.getListener().resourceSyncInfoChanged(possibleIgnores.toArray(new IResource[possibleIgnores.size()])); |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| |
| /** |
| * Returns the members of this folder including deleted resources with sync info, |
| * but excluding special resources such as CVS subdirectories. |
| * |
| * @param folder the container to list |
| * @return the array of members |
| */ |
| public IResource[] members(IContainer folder) throws CVSException { |
| if (! isValid(folder)) return new IResource[0]; |
| try { |
| beginOperation(); |
| if (folder.getType() != IResource.ROOT) { |
| // ensure that the sync info is cached so any required phantoms are created |
| cacheResourceSyncForChildren(folder, false); |
| } |
| } catch (CVSException e) { |
| if (!isCannotModifySynchronizer(e) && !isResourceNotFound(e)) { |
| throw e; |
| } |
| } finally { |
| endOperation(); |
| } |
| try { |
| |
| return synchronizerCache.members(folder); |
| |
| } catch (CoreException e) { |
| throw CVSException.wrapException(e); |
| } |
| } |
| |
| private boolean isCannotModifySynchronizer(CVSException e) { |
| // IResourceStatus.WORKSPACE_LOCKED can occur if the resource sync is loaded |
| // during the POST_CHANGE delta phase. |
| // CVSStatus.FAILED_TO_CACHE_SYNC_INFO can occur if the resource sync is loaded |
| // when no scheduling rule is held. |
| return (e.getStatus().getCode() == IResourceStatus.WORKSPACE_LOCKED |
| || e.getStatus().getCode() == CVSStatus.FAILED_TO_CACHE_SYNC_INFO); |
| } |
| |
| private boolean isResourceNotFound(CVSException e) { |
| return e.getStatus().getCode() == IResourceStatus.RESOURCE_NOT_FOUND; |
| } |
| |
| /** |
| * Begins a batch of operations in order to optimize sync file writing. |
| * The provided scheduling rule indicates the resources |
| * that the resources affected by the operation while the returned scheduling rule |
| * is the rule obtained by the lock. It may differ from the provided rule as it must |
| * encompass any sync files that may change as a result of the operation. |
| */ |
| public ISchedulingRule beginBatching(ISchedulingRule resourceRule, IProgressMonitor monitor) { |
| return resourceLock.acquire(resourceRule, this /* IFlushOperation */, monitor); |
| } |
| |
| /** |
| * Ends a batch of operations. The provided rule must be the one that was returned |
| * by the corresponding call to beginBatching. |
| * <p> |
| * Progress cancellation is ignored while writting the cache to disk. This |
| * is to ensure cache to disk consistency. |
| * </p> |
| * |
| * @param monitor the progress monitor, may be null |
| * @exception CVSException with a status with code <code>COMMITTING_SYNC_INFO_FAILED</code> |
| * if all the CVS sync information could not be written to disk. |
| */ |
| public void endBatching(ISchedulingRule rule, IProgressMonitor monitor) throws CVSException { |
| try { |
| resourceLock.release(rule, monitor); |
| } catch (TeamException e) { |
| throw CVSException.wrapException(e); |
| } |
| } |
| |
| /* |
| * Callback which is invoked when the batching resource lock is released |
| * or when a flush is requested (see beginBatching(IResource)). |
| * |
| * @see org.eclipse.team.internal.ccvs.core.syncinfo.ReentrantLock.IRunnableOnExit#run(org.eclipse.team.internal.ccvs.core.syncinfo.ReentrantLock.ThreadInfo, org.eclipse.core.runtime.IProgressMonitor) |
| */ |
| @Override |
| public void flush(final ThreadInfo info, IProgressMonitor monitor) throws CVSException { |
| if (info != null && !info.isEmpty()) { |
| try { |
| ResourcesPlugin.getWorkspace().run((IWorkspaceRunnable) pm -> { |
| IStatus status = commitCache(info, pm); |
| if (!status.isOK()) { |
| throw new CVSException(status); |
| } |
| }, null, 0 /* no flags */, monitor); |
| } catch (CoreException e) { |
| throw CVSException.wrapException(e); |
| } |
| } |
| } |
| |
| /* |
| * Begin an access to the internal data structures of the synchronizer |
| */ |
| private void beginOperation() { |
| try { |
| // Do not try to acquire the lock if the resources tree is locked |
| // The reason for this is that during the resource delta phase (i.e. when the tree is locked) |
| // the workspace lock is held. If we obtain our lock, there is |
| // a chance of dealock. It is OK if we don't as we are still protected |
| // by scheduling rules and the workspace lock. |
| if (ResourcesPlugin.getWorkspace().isTreeLocked()) return; |
| } catch (RuntimeException e) { |
| // If we are not active, throw a cancel. Otherwise, propogate it. |
| // (see bug 78303) |
| if (Platform.getBundle(CVSProviderPlugin.ID).getState() == Bundle.ACTIVE) { |
| throw e; |
| } else { |
| throw new OperationCanceledException(); |
| } |
| } |
| lock.acquire(); |
| } |
| |
| /* |
| * End an access to the internal data structures of the synchronizer |
| */ |
| private void endOperation() { |
| try { |
| // See beginOperation() for a description of why the lock is not obtained when the tree is locked |
| if (ResourcesPlugin.getWorkspace().isTreeLocked()) return; |
| } catch (RuntimeException e) { |
| // If we are not active, throw a cancel. Otherwise, propogate it. |
| // (see bug 78303) |
| if (Platform.getBundle(CVSProviderPlugin.ID).getState() == Bundle.ACTIVE) { |
| throw e; |
| } else { |
| throw new OperationCanceledException(); |
| } |
| } |
| lock.release(); |
| } |
| |
| /** |
| * Flush the sync information from the in-memory cache to disk and purge |
| * the entries from the cache. |
| * <p> |
| * Recursively flushes the sync information for all resources |
| * below the root to disk and purges the entries from memory |
| * so that the next time it is accessed it will be retrieved from disk. |
| * May flush more sync information than strictly needed, but never less. |
| * </p> |
| * |
| * @param root the root of the subtree to purge |
| * @param deep purge sync from child folders |
| * @param monitor the progress monitor, may be null |
| */ |
| public void flush(IContainer root, boolean deep, IProgressMonitor monitor) throws CVSException { |
| monitor = Policy.monitorFor(monitor); |
| monitor.beginTask(null, 10); |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(root, Policy.subMonitorFor(monitor, 1)); |
| try { |
| beginOperation(); |
| try { |
| // Flush changes to disk |
| resourceLock.flush(Policy.subMonitorFor(monitor, 8)); |
| } catch (TeamException e) { |
| throw CVSException.wrapException(e); |
| } finally { |
| // Purge the in-memory cache |
| sessionPropertyCache.purgeCache(root, deep); |
| } |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, Policy.subMonitorFor(monitor, 1)); |
| monitor.done(); |
| } |
| } |
| |
| public void deconfigure(final IProject project, IProgressMonitor monitor) throws CVSException { |
| monitor = Policy.monitorFor(monitor); |
| monitor.beginTask(null, 100); |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(project, Policy.subMonitorFor(monitor, 10)); |
| // Flush the sync info |
| flush(project, true /* deep */, Policy.subMonitorFor(monitor, 80)); |
| |
| purgeDirtyCache(project, Policy.subMonitorFor(monitor, 5)); |
| |
| // forget about pruned folders however the top level pruned folder will have resource sync (e.g. |
| // a line in the Entry file). As a result the folder is managed but is not a CVS folder. |
| synchronizerCache.purgeCache(project, true); |
| } finally { |
| if (rule != null) endBatching(rule, Policy.subMonitorFor(monitor, 5)); |
| monitor.done(); |
| } |
| } |
| |
| /** |
| * Called to notify the synchronizer that meta files have changed on disk, outside |
| * of the workbench. The cache will be flushed for this folder and it's immediate |
| * children and appropriate state change events are broadcasts to state change |
| * listeners. |
| */ |
| public void ignoreFilesChanged(IContainer[] roots) throws CVSException { |
| for (IContainer container : roots) { |
| if (container.exists()) { |
| ISchedulingRule rule = null; |
| try { |
| Set<IResource> changed = new HashSet<>(); |
| rule = beginBatching(container, null); |
| try { |
| beginOperation(); |
| |
| // Record the previous ignore pattterns |
| FileNameMatcher oldIgnores = null; |
| if (sessionPropertyCache.isFolderSyncInfoCached(container)) { |
| oldIgnores = cacheFolderIgnores(container); |
| } |
| |
| // Purge the cached state for direct children of the container |
| changed.addAll(Arrays.asList( |
| sessionPropertyCache.purgeCache(container, oldIgnores == null /*flush deeply if the old patterns are not known*/))); |
| |
| // Purge the state for any children of previously ignored containers |
| if (oldIgnores != null) { |
| FileNameMatcher newIgnores = cacheFolderIgnores(container); |
| try { |
| IResource[] members = container.members(); |
| for (IResource resource : members) { |
| if (resource.getType() == IResource.FOLDER) { |
| String name = resource.getName(); |
| if (oldIgnores.match(name) && !newIgnores.match(name)) { |
| changed.addAll(Arrays.asList( |
| sessionPropertyCache.purgeCache((IContainer)resource, true /*flush deeply*/))); |
| } |
| } |
| } |
| } catch (CoreException e) { |
| // Just log and continue |
| CVSProviderPlugin.log(e); |
| } |
| } |
| } finally { |
| endOperation(); |
| } |
| if (!changed.isEmpty()) { |
| ResourceStateChangeListeners.getListener().resourceSyncInfoChanged( |
| changed.toArray(new IResource[changed.size()])); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| } |
| } |
| |
| public void syncFilesChangedExternally(IContainer[] changedMetaFiles, IFile[] externalDeletions) throws CVSException { |
| List<IResource> changed = new ArrayList<>(); |
| for (IContainer container : changedMetaFiles) { |
| if (!isWithinActiveOperationScope(container)) { |
| changed.addAll(Arrays.asList( |
| sessionPropertyCache.purgeCache(container, false /*don't flush children*/))); |
| } |
| } |
| for (IFile file : externalDeletions) { |
| if (!isWithinActiveOperationScope(file)) { |
| sessionPropertyCache.purgeCache(file.getParent(), false /*don't flush children*/); |
| changed.add(file); |
| } |
| } |
| if (!changed.isEmpty()) { |
| ResourceStateChangeListeners.getListener().externalSyncInfoChange( |
| changed.toArray(new IResource[changed.size()])); |
| } |
| } |
| |
| /* |
| * The resource is about to be deleted by the move delete hook. |
| * In all cases (except when the resource doesn't exist), this method |
| * will indicate that the dirty state of the parent needs to be recomputed. |
| * For managed resources, it will move the cached sync info from the session |
| * property cache into the synchronizer cache, purging the session cache. |
| * @param resource the resource about to be deleted. |
| * <p> |
| * Note that this method is not recursive. Hence, for managed resources |
| * |
| * @returns whether children need to be prepared |
| * @throws CVSException |
| */ |
| /* private */ boolean prepareForDeletion(IResource resource) throws CVSException { |
| if (!resource.exists()) return false; |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(resource, null); |
| try { |
| beginOperation(); |
| // Flush the dirty info for the resource and it's ancestors. |
| // Although we could be smarter, we need to do this because the |
| // deletion may fail. |
| adjustDirtyStateRecursively(resource, RECOMPUTE_INDICATOR); |
| if (resource.getType() == IResource.FILE) { |
| byte[] syncBytes = getSyncBytes(resource); |
| if (syncBytes != null) { |
| if (ResourceSyncInfo.isAddition(syncBytes)) { |
| deleteResourceSync(resource); |
| } else { |
| syncBytes = convertToDeletion(syncBytes); |
| synchronizerCache.setCachedSyncBytes(resource, syncBytes, true); |
| } |
| sessionPropertyCache.purgeResourceSyncCache(resource); |
| resourceChanged(resource); |
| } |
| return false; |
| } else { |
| IContainer container = (IContainer)resource; |
| if (container.getType() == IResource.PROJECT) { |
| synchronizerCache.flush((IProject)container); |
| return false; |
| } else { |
| // Move the folder sync info into phantom space |
| FolderSyncInfo info = getFolderSync(container); |
| if (info == null) return false; |
| synchronizerCache.setCachedFolderSync(container, info, true); |
| folderChanged(container); |
| // move the resource sync as well |
| byte[] syncBytes = getSyncBytes(resource); |
| synchronizerCache.setCachedSyncBytes(resource, syncBytes, true); |
| sessionPropertyCache.purgeResourceSyncCache(container); |
| sessionPropertyCache.purgeCache(container, false); |
| return true; |
| } |
| } |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| |
| /** |
| * The resource has been deleted. Make sure any cached state is cleared. |
| * This is needed because the move/delete hook is not invoked in all situations |
| * (e.g. external deletion). |
| * |
| * @param resource |
| * @throws CVSException |
| */ |
| protected void handleDeleted(IResource resource) throws CVSException { |
| if (resource.exists()) return; |
| try { |
| beginOperation(); |
| adjustDirtyStateRecursively(resource, RECOMPUTE_INDICATOR); |
| } finally { |
| endOperation(); |
| } |
| } |
| |
| /** |
| * The resource has been added. Make sure any cached state is cleared. |
| * This is needed because the add hook is not invoked in all situations |
| * (e.g. external addition). |
| * |
| * @param resource |
| * @throws CVSException |
| */ |
| protected void handleAdded(IResource resource) throws CVSException { |
| if (!resource.exists()) return; |
| try { |
| beginOperation(); |
| adjustDirtyStateRecursively(resource, RECOMPUTE_INDICATOR); |
| } finally { |
| endOperation(); |
| } |
| } |
| |
| /** |
| * Prepare for the deletion of the target resource from within |
| * the move/delete hook. The method is invoked by both the |
| * deleteFile/Folder methods and for the source resource |
| * of moveFile/Folder. This method will move the cached sync info |
| * into the phantom (ISynchronizer) cache so that outgoing deletions |
| * and known remote folders are preserved. |
| * |
| * @param resource |
| * @param monitor |
| * @throws CVSException |
| */ |
| public void prepareForDeletion(IResource resource, IProgressMonitor monitor) throws CVSException { |
| // Move sync info to phantom space for the resource and all it's children |
| monitor = Policy.monitorFor(monitor); |
| try { |
| beginOperation(); |
| monitor.beginTask(null, 100); |
| try { |
| resource.accept(new IResourceVisitor() { |
| public boolean visit(IResource innerResource) throws CoreException { |
| try { |
| return prepareForDeletion(innerResource); |
| } catch (CVSException e) { |
| CVSProviderPlugin.log(e); |
| throw new CoreException(e.getStatus()); |
| } |
| } |
| }); |
| } catch (CoreException e) { |
| throw CVSException.wrapException(e); |
| } |
| } finally { |
| endOperation(); |
| monitor.done(); |
| } |
| } |
| |
| /** |
| * If not already cached, loads and caches the resource sync for the children of the container. |
| * Folder must exist and must not be the workspace root. |
| * |
| * @param container the container |
| */ |
| private void cacheResourceSyncForChildren(IContainer container, boolean canModifyWorkspace) throws CVSException { |
| // don't try to load if the information is already cached |
| if (! getSyncInfoCacheFor(container).isResourceSyncInfoCached(container)) { |
| // load the sync info from disk |
| byte[][] infos; |
| // do not load the sync info for resources that are linked |
| if (isLinkedResource(container)) { |
| infos = null; |
| } else { |
| infos = SyncFileWriter.readAllResourceSync(container); |
| } |
| try { |
| if (infos != null) { |
| for (byte[] syncBytes : infos) { |
| IPath name = new Path(null, getName(syncBytes)); |
| IResource resource; |
| if (isFolder(syncBytes)) { |
| resource = container.getFolder(name); |
| } else { |
| resource = container.getFile(name); |
| } |
| getSyncInfoCacheFor(resource).setCachedSyncBytes(resource, syncBytes, canModifyWorkspace); |
| } |
| } |
| getSyncInfoCacheFor(container).setResourceSyncInfoCached(container); |
| } catch (CVSException e) { |
| if (Policy.DEBUG_METAFILE_CHANGES) { |
| System.err.println("Failed to cache Entries for folder " + container.getFullPath()); //$NON-NLS-1$ |
| } |
| throw e; |
| } |
| } |
| } |
| |
| /** |
| * If not already cached, loads and caches the folder sync for the |
| * container. Folder must exist and must not be the workspace root. |
| * |
| * @param container the container |
| */ |
| private void cacheFolderSync(IContainer container) throws CVSException { |
| // don't try to load if the information is already cached |
| if (! getSyncInfoCacheFor(container).isFolderSyncInfoCached(container)) { |
| // load the sync info from disk |
| FolderSyncInfo info; |
| // do not load the sync info for resources that are linked |
| if (isLinkedResource(container)) { |
| info = null; |
| } else { |
| info = SyncFileWriter.readFolderSync(container); |
| } |
| getSyncInfoCacheFor(container).setCachedFolderSync(container, info, false); |
| } |
| } |
| |
| private boolean isLinkedResource(IResource resource) { |
| return CVSWorkspaceRoot.isLinkedResource(resource); |
| } |
| |
| /** |
| * Load the sync info for the given resource from disk |
| * @param resource |
| * @return byte[] |
| */ |
| private byte[] getSyncBytesFromDisk(IResource resource) throws CVSException { |
| byte[][] infos = SyncFileWriter.readAllResourceSync(resource.getParent()); |
| if (infos == null) return null; |
| for (byte[] syncBytes : infos) { |
| if (resource.getName().equals(getName(syncBytes))) { |
| return syncBytes; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Commits the cache after a series of operations. |
| * |
| * Will return STATUS_OK unless there were problems writting sync |
| * information to disk. If an error occurs a multistatus is returned |
| * with the list of reasons for the failures. Failures are recovered, |
| * and all changed resources are given a chance to be written to disk. |
| * |
| * @param monitor the progress monitor, may be null |
| */ |
| /* internal use only */ IStatus commitCache(ThreadInfo threadInfo, IProgressMonitor monitor) { |
| if (threadInfo.isEmpty()) { |
| return SyncInfoCache.STATUS_OK; |
| } |
| List<IStatus> errors = new ArrayList<>(); |
| try { |
| /*** prepare operation ***/ |
| // find parents of changed resources |
| IResource[] changedResources = threadInfo.getChangedResources(); |
| IContainer[] changedFolders; |
| if (threadInfo instanceof CVSThreadInfo) { |
| changedFolders = ((CVSThreadInfo)threadInfo).getChangedFolders(); |
| } else { |
| changedFolders = new IContainer[0]; |
| } |
| Set<IContainer> dirtyParents = new HashSet<>(); |
| for (IResource resource : changedResources) { |
| IContainer folder = resource.getParent(); |
| dirtyParents.add(folder); |
| } |
| |
| monitor = Policy.monitorFor(monitor); |
| int numDirty = dirtyParents.size(); |
| int numResources = changedFolders.length + numDirty; |
| monitor.beginTask(null, numResources); |
| if(monitor.isCanceled()) { |
| monitor.subTask(CVSMessages.EclipseSynchronizer_UpdatingSyncEndOperationCancelled); |
| } else { |
| monitor.subTask(CVSMessages.EclipseSynchronizer_UpdatingSyncEndOperation); |
| } |
| |
| /*** write sync info to disk ***/ |
| // folder sync info changes |
| for (IContainer folder : changedFolders) { |
| if (folder.exists() && folder.getType() != IResource.ROOT) { |
| try { |
| beginOperation(); |
| FolderSyncInfo info = sessionPropertyCache.getCachedFolderSync(folder, true); |
| // Do not write the folder sync for linked resources |
| if (info == null) { |
| // deleted folder sync info since we loaded it |
| // (but don't overwrite the sync info for linked folders |
| if (!isLinkedResource(folder)) |
| SyncFileWriter.deleteFolderSync(folder); |
| dirtyParents.remove(folder); |
| } else { |
| // modified or created new folder sync info since we loaded it |
| SyncFileWriter.writeFolderSync(folder, info); |
| } |
| } catch(CVSException e) { |
| try { |
| sessionPropertyCache.purgeCache(folder, true /* deep */); |
| } catch(CVSException pe) { |
| errors.add(pe.getStatus()); |
| } |
| errors.add(e.getStatus()); |
| } finally { |
| endOperation(); |
| } |
| } |
| monitor.worked(1); |
| } |
| |
| // update progress for parents we will skip because they were deleted |
| monitor.worked(numDirty - dirtyParents.size()); |
| |
| // resource sync info changes |
| for (Iterator it = dirtyParents.iterator(); it.hasNext();) { |
| IContainer folder = (IContainer) it.next(); |
| if (folder.exists() && folder.getType() != IResource.ROOT) { |
| // write sync info for all children in one go |
| try { |
| beginOperation(); |
| List<byte[]> infos = new ArrayList<>(); |
| IResource[] children = folder.members(true); |
| for (IResource resource : children) { |
| byte[] syncBytes = getSyncBytes(resource); |
| if (syncBytes != null) { |
| infos.add(syncBytes); |
| } |
| } |
| // do not overwrite the sync info for linked resources |
| if (infos.size() > 0 || !isLinkedResource(folder)) |
| SyncFileWriter.writeAllResourceSync(folder, |
| infos.toArray(new byte[infos.size()][])); |
| } catch(CVSException e) { |
| try { |
| sessionPropertyCache.purgeCache(folder, false /* depth 1 */); |
| } catch(CVSException pe) { |
| errors.add(pe.getStatus()); |
| } |
| errors.add(e.getStatus()); |
| } catch (CoreException e) { |
| try { |
| sessionPropertyCache.purgeCache(folder, false /* depth 1 */); |
| } catch(CVSException pe) { |
| errors.add(pe.getStatus()); |
| } |
| errors.add(e.getStatus()); |
| } finally { |
| endOperation(); |
| } |
| } |
| monitor.worked(1); |
| } |
| |
| /*** broadcast events ***/ |
| monitor.subTask(CVSMessages.EclipseSynchronizer_NotifyingListeners); |
| Set<IResource> allChanges = new HashSet<>(); |
| allChanges.addAll(Arrays.asList(changedResources)); |
| allChanges.addAll(Arrays.asList(changedFolders)); |
| allChanges.addAll(dirtyParents); |
| IResource[] resources = allChanges.toArray( |
| new IResource[allChanges.size()]); |
| broadcastResourceStateChanges(resources); |
| if ( ! errors.isEmpty()) { |
| MultiStatus status = new MultiStatus(CVSProviderPlugin.ID, |
| CVSStatus.COMMITTING_SYNC_INFO_FAILED, |
| CVSMessages.EclipseSynchronizer_ErrorCommitting, |
| null); |
| for (int i = 0; i < errors.size(); i++) { |
| status.merge(errors.get(i)); |
| } |
| return status; |
| } |
| return SyncInfoCache.STATUS_OK; |
| } finally { |
| monitor.done(); |
| } |
| } |
| |
| /** |
| * Broadcasts the resource state changes for the given resources to CVS Provider Plugin |
| */ |
| void broadcastResourceStateChanges(IResource[] resources) { |
| if (resources.length > 0) { |
| ResourceStateChangeListeners.getListener().resourceSyncInfoChanged(resources); |
| } |
| } |
| |
| /** |
| * Returns the resource sync info for the resource; null if none. |
| * Parent must exist and must not be the workspace root. |
| * The resource sync info for the children of the parent container MUST ALREADY BE CACHED. |
| * |
| * @param resource the resource |
| * @return the resource sync info for the resource, or null |
| * @see #cacheResourceSyncForChildren |
| */ |
| private byte[] getCachedSyncBytes(IResource resource) throws CVSException { |
| return getSyncInfoCacheFor(resource).getCachedSyncBytes(resource, true); |
| } |
| |
| /** |
| * Returns the resource sync info for the resource; null if none. |
| * Parent must exist and must not be the workspace root. |
| * The resource sync info for the children of the parent container MUST ALREADY BE CACHED. |
| * |
| * @param resource the resource |
| * @return the resource sync info for the resource, or null |
| * @see #cacheResourceSyncForChildren |
| */ |
| private void setCachedSyncBytes(IResource resource, byte[] syncBytes) throws CVSException { |
| getSyncInfoCacheFor(resource).setCachedSyncBytes(resource, syncBytes, true); |
| resourceChanged(resource); |
| } |
| |
| /** |
| * Sets the resource sync info for the resource; if null, deletes it. Parent |
| * must exist and must not be the workspace root. The resource sync info for |
| * the children of the parent container MUST ALREADY BE CACHED. |
| * |
| * @param resource the resource |
| * @param info the new resource sync info |
| * @see #cacheResourceSyncForChildren |
| */ |
| private void setCachedResourceSync(IResource resource, ResourceSyncInfo info) throws CVSException { |
| //todo |
| byte[] syncBytes = null; |
| if (info != null) syncBytes = info.getBytes(); |
| getSyncInfoCacheFor(resource).setCachedSyncBytes(resource, syncBytes, true); |
| } |
| |
| /** |
| * If not already cached, loads and caches the folder ignores sync for the container. |
| * Folder must exist and must not be the workspace root. |
| * |
| * @param container the container |
| * @return the folder ignore patterns, or an empty array if none |
| */ |
| private FileNameMatcher cacheFolderIgnores(IContainer container) throws CVSException { |
| return sessionPropertyCache.getFolderIgnores(container, true); |
| } |
| |
| /** |
| * Sets the array of folder ignore patterns for the container, must not be null. |
| * Folder must exist and must not be the workspace root. |
| * |
| * @param container the container |
| * @param ignores the array of ignore patterns |
| */ |
| private void setCachedFolderIgnores(IContainer container, String[] ignores) throws CVSException { |
| sessionPropertyCache.setCachedFolderIgnores(container, ignores); |
| } |
| |
| /* |
| * Recursively adds to the possibleIgnores list all children of the given |
| * folder that can be ignored. This method may only be invoked when a |
| * schedling rule for the given foldr is held and when the CVs sync lock is |
| * held. |
| * |
| * @param folder the folder to be searched |
| * @param possibleIgnores the list of IResources that can be ignored |
| */ |
| private void accumulateNonManagedChildren(IContainer folder, List<IResource> possibleIgnores) throws CVSException { |
| try { |
| cacheResourceSyncForChildren(folder, true /* can modify workspace */); |
| IResource[] children = folder.members(); |
| List<IResource> folders = new ArrayList<>(); |
| // deal with all files first and then folders to be otimized for caching scheme |
| for (IResource child : children) { |
| if(getCachedSyncBytes(child)==null) { |
| possibleIgnores.add(child); |
| } |
| if(child.getType()!=IResource.FILE) { |
| folders.add(child); |
| } |
| } |
| for (Iterator iter = folders.iterator(); iter.hasNext();) { |
| IContainer child = (IContainer) iter.next(); |
| accumulateNonManagedChildren(child, possibleIgnores); |
| } |
| } catch(CoreException e) { |
| throw CVSException.wrapException(e); |
| } |
| } |
| |
| /** |
| * Add the entry to the CVS/Notify file. We are not initially concerned with efficiency |
| * since edit/unedit are typically issued on a small set of files. |
| * |
| * XXX If there was a previous notify entry for the resource, it is replaced. This is |
| * probably not the proper behavior (see EclipseFile). |
| * |
| * A value of null for info indicates that any entry for the given |
| * resource is to be removed from the Notify file. |
| * |
| * @param resource |
| * @param info |
| */ |
| public void setNotifyInfo(IResource resource, NotifyInfo info) throws CVSException { |
| NotifyInfo[] infos = SyncFileWriter.readAllNotifyInfo(resource.getParent()); |
| if (infos == null) { |
| // if the file is empty and we are removing an entry, just return; |
| if (info == null) return; |
| infos = new NotifyInfo[] { info }; |
| } else { |
| Map<String, NotifyInfo> infoMap = new HashMap<>(); |
| for (NotifyInfo notifyInfo : infos) { |
| infoMap.put(notifyInfo.getName(), notifyInfo); |
| } |
| if (info == null) { |
| // if the info is null, remove the entry |
| infoMap.remove(resource.getName()); |
| } else { |
| // add the new entry to the list |
| infoMap.put(info.getName(), info); |
| } |
| |
| NotifyInfo[] newInfos = new NotifyInfo[infoMap.size()]; |
| int i = 0; |
| for (Iterator iter = infoMap.values().iterator(); iter.hasNext();) { |
| newInfos[i++] = (NotifyInfo) iter.next(); |
| } |
| infos = newInfos; |
| } |
| SyncFileWriter.writeAllNotifyInfo(resource.getParent(), infos); |
| } |
| |
| /** |
| * Method getNotifyInfo. |
| * @param resource |
| * @return NotifyInfo |
| */ |
| public NotifyInfo getNotifyInfo(IResource resource) throws CVSException { |
| NotifyInfo[] infos = SyncFileWriter.readAllNotifyInfo(resource.getParent()); |
| if (infos == null) return null; |
| for (NotifyInfo notifyInfo : infos) { |
| if (notifyInfo.getName().equals(resource.getName())) { |
| return notifyInfo; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Method deleteNotifyInfo. |
| * @param resource |
| */ |
| public void deleteNotifyInfo(IResource resource) throws CVSException { |
| NotifyInfo[] infos = SyncFileWriter.readAllNotifyInfo(resource.getParent()); |
| if (infos == null) return; |
| Map<String, NotifyInfo> infoMap = new HashMap<>(); |
| for (NotifyInfo notifyInfo : infos) { |
| infoMap.put(notifyInfo.getName(), notifyInfo); |
| } |
| infoMap.remove(resource.getName()); |
| NotifyInfo[] newInfos = new NotifyInfo[infoMap.size()]; |
| int i = 0; |
| for (Iterator iter = infoMap.values().iterator(); iter.hasNext();) { |
| newInfos[i++] = (NotifyInfo) iter.next(); |
| } |
| SyncFileWriter.writeAllNotifyInfo(resource.getParent(), newInfos); |
| } |
| |
| /** |
| * Add the entry to the CVS/Baserev file. We are not initially concerned |
| * with efficiency since edit/unedit are typically issued on a small set of |
| * files. |
| * |
| * XXX If there was a previous notify entry for the resource, it is replaced. This is |
| * probably not the proper behavior (see EclipseFile). |
| * |
| * @param resource |
| * @param info |
| */ |
| public void setBaserevInfo(IResource resource, BaserevInfo info) throws CVSException { |
| BaserevInfo[] infos = SyncFileWriter.readAllBaserevInfo(resource.getParent()); |
| if (infos == null) { |
| infos = new BaserevInfo[] { info }; |
| } else { |
| Map<String, BaserevInfo> infoMap = new HashMap<>(); |
| for (BaserevInfo i : infos) { |
| infoMap.put(i.getName(), i); |
| } |
| infoMap.put(info.getName(), info); |
| BaserevInfo[] newInfos = new BaserevInfo[infoMap.size()]; |
| int i = 0; |
| for (Iterator iter = infoMap.values().iterator(); iter.hasNext();) { |
| newInfos[i++] = (BaserevInfo) iter.next(); |
| } |
| infos = newInfos; |
| } |
| SyncFileWriter.writeAllBaserevInfo(resource.getParent(), infos); |
| } |
| |
| /** |
| * Method getBaserevInfo. |
| * @param resource |
| * @return BaserevInfo |
| */ |
| public BaserevInfo getBaserevInfo(IResource resource) throws CVSException { |
| BaserevInfo[] infos = SyncFileWriter.readAllBaserevInfo(resource.getParent()); |
| if (infos == null) return null; |
| for (BaserevInfo info : infos) { |
| if (info.getName().equals(resource.getName())) { |
| return info; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Method deleteNotifyInfo. |
| * @param resource |
| */ |
| public void deleteBaserevInfo(IResource resource) throws CVSException { |
| BaserevInfo[] infos = SyncFileWriter.readAllBaserevInfo(resource.getParent()); |
| if (infos == null) return; |
| Map<String, BaserevInfo> infoMap = new HashMap<>(); |
| for (BaserevInfo info : infos) { |
| infoMap.put(info.getName(), info); |
| } |
| infoMap.remove(resource.getName()); |
| BaserevInfo[] newInfos = new BaserevInfo[infoMap.size()]; |
| int i = 0; |
| for (Iterator iter = infoMap.values().iterator(); iter.hasNext();) { |
| newInfos[i++] = (BaserevInfo) iter.next(); |
| } |
| SyncFileWriter.writeAllBaserevInfo(resource.getParent(), newInfos); |
| } |
| |
| public void copyFileToBaseDirectory(final IFile file, IProgressMonitor monitor) throws CVSException { |
| monitor = Policy.monitorFor(monitor); |
| monitor.beginTask(null, 100); |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(file, Policy.subMonitorFor(monitor, 10)); |
| ResourceSyncInfo info = getResourceSync(file); |
| // The file must exist remotely and locally |
| if (info == null || info.isAdded() || info.isDeleted()) |
| return; |
| SyncFileWriter.writeFileToBaseDirectory(file, Policy.subMonitorFor(monitor, 80)); |
| resourceChanged(file); |
| } finally { |
| if (rule != null) endBatching(rule, Policy.subMonitorFor(monitor, 10)); |
| monitor.done(); |
| } |
| } |
| |
| public void restoreFileFromBaseDirectory(final IFile file, IProgressMonitor monitor) throws CVSException { |
| monitor = Policy.monitorFor(monitor); |
| monitor.beginTask(null, 100); |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(file, Policy.subMonitorFor(monitor, 10)); |
| ResourceSyncInfo info = getResourceSync(file); |
| // The file must exist remotely |
| if (info == null || info.isAdded()) |
| return; |
| SyncFileWriter.restoreFileFromBaseDirectory(file, Policy.subMonitorFor(monitor, 80)); |
| resourceChanged(file); |
| } finally { |
| if (rule != null) endBatching(rule, Policy.subMonitorFor(monitor, 10)); |
| monitor.done(); |
| } |
| } |
| |
| public void deleteFileFromBaseDirectory(final IFile file, IProgressMonitor monitor) throws CVSException { |
| ResourceSyncInfo info = getResourceSync(file); |
| // The file must exist remotely |
| if (info == null || info.isAdded()) |
| return; |
| SyncFileWriter.deleteFileFromBaseDirectory(file, monitor); |
| } |
| |
| /** |
| * Method isSyncInfoLoaded returns true if all the sync info for the |
| * provided resources is loaded into the internal cache. |
| * |
| * @param resources |
| * @param i |
| * @return boolean |
| */ |
| public boolean isSyncInfoLoaded(IResource[] resources, int depth) throws CVSException { |
| // get the folders involved |
| IContainer[] folders = getParentFolders(resources, depth); |
| // for all folders that have a CVS folder, ensure the sync info is cached |
| for (IContainer parent : folders) { |
| if (!getSyncInfoCacheFor(parent).isSyncInfoLoaded(parent)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Method ensureSyncInfoLoaded loads all the relevent sync info into the cache. |
| * This method can only be invoked when the workspace is open for modification. |
| * in other words it cannot be invoked from inside a POST_CHANGE delta listener. |
| * @param resources |
| * @param i |
| * @return Object |
| */ |
| public void ensureSyncInfoLoaded(IResource[] resources, int depth) throws CVSException { |
| // get the folders involved |
| IContainer[] folders = getParentFolders(resources, depth); |
| // Cache the sync info for all the folders |
| for (IContainer parent : folders) { |
| ISchedulingRule rule = null; |
| try { |
| rule = beginBatching(parent, null); |
| try { |
| beginOperation(); |
| cacheResourceSyncForChildren(parent, true /* can modify workspace */); |
| cacheFolderSync(parent); |
| cacheFolderIgnores(parent); |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| } |
| |
| /* |
| * Collect the projects and parent folders of the resources since |
| * thats were the sync info is kept. |
| */ |
| private IContainer[] getParentFolders(IResource[] resources, int depth) throws CVSException { |
| final Set<IResource> folders = new HashSet<>(); |
| for (IResource resource : resources) { |
| folders.add(resource.getProject()); |
| if (resource.getType() != IResource.PROJECT) { |
| folders.add(resource.getParent()); |
| } |
| // use the depth to gather child folders when appropriate |
| if (depth != IResource.DEPTH_ZERO) { |
| try { |
| resource.accept(new IResourceVisitor() { |
| public boolean visit(IResource innerResource) throws CoreException { |
| if (innerResource.getType() == IResource.FOLDER) |
| folders.add(innerResource); |
| // let the depth determine who we visit |
| return true; |
| } |
| }, depth, false); |
| } catch (CoreException e) { |
| throw CVSException.wrapException(e); |
| } |
| } |
| } |
| return folders.toArray(new IContainer[folders.size()]); |
| } |
| |
| /** |
| * Perform sync info batching within the context of the given resource |
| * scheduling rule while running the given ICVSRunnable. |
| * @param runnable |
| * @param monitor |
| * @throws CVSException |
| */ |
| public void run(ISchedulingRule resourceRule, ICVSRunnable runnable, IProgressMonitor monitor) throws CVSException { |
| monitor = Policy.monitorFor(monitor); |
| monitor.beginTask(null, 100); |
| ISchedulingRule rule = beginBatching(resourceRule, Policy.subMonitorFor(monitor, 1)); |
| try { |
| runnable.run(Policy.subMonitorFor(monitor, 98)); |
| } finally { |
| if (rule != null) endBatching(rule, Policy.subMonitorFor(monitor, 1)); |
| monitor.done(); |
| } |
| } |
| |
| /** |
| * Method isEdited returns true if a "cvs edit" was performed on the given |
| * file and no commit or unedit has yet been performed. |
| * @param iResource |
| * @return boolean |
| */ |
| public boolean isEdited(IFile resource) { |
| return SyncFileWriter.isEdited(resource); |
| } |
| |
| /* package */ void adjustDirtyStateRecursively(IResource resource, String indicator) throws CVSException { |
| if (resource.getType() == IResource.ROOT) return; |
| try { |
| beginOperation(); |
| |
| if (getSyncInfoCacheFor(resource).cachesDirtyState()) { |
| if (indicator == getDirtyIndicator(resource)) { |
| return; |
| } |
| getSyncInfoCacheFor(resource).setDirtyIndicator(resource, indicator); |
| } |
| |
| if (Policy.DEBUG_DIRTY_CACHING) { |
| debug(resource, indicator, "adjusting dirty state"); //$NON-NLS-1$ |
| } |
| |
| IContainer parent = resource.getParent(); |
| if(indicator == NOT_DIRTY_INDICATOR) { |
| adjustDirtyStateRecursively(parent, RECOMPUTE_INDICATOR); |
| } |
| |
| if(indicator == RECOMPUTE_INDICATOR) { |
| adjustDirtyStateRecursively(parent, RECOMPUTE_INDICATOR); |
| } |
| |
| if(indicator == IS_DIRTY_INDICATOR) { |
| adjustDirtyStateRecursively(parent, indicator); |
| } |
| } finally { |
| endOperation(); |
| } |
| } |
| |
| protected String getDirtyIndicator(IResource resource) throws CVSException { |
| // Do a check outside the lock for the dirty indicator |
| String indicator = getSyncInfoCacheFor(resource).getDirtyIndicator(resource, false /* not thread safe */); |
| if (indicator != null) |
| return indicator; |
| try { |
| beginOperation(); |
| return getSyncInfoCacheFor(resource).getDirtyIndicator(resource, true); |
| } finally { |
| endOperation(); |
| } |
| } |
| |
| /* |
| * Mark the given resource as either modified or clean using a persistant |
| * property. Do nothing if the modified state is already what we want. |
| * Return true if the modification state was changed. |
| */ |
| protected void setDirtyIndicator(IResource resource, boolean modified) throws CVSException { |
| String indicator = modified ? IS_DIRTY_INDICATOR : NOT_DIRTY_INDICATOR; |
| // set the dirty indicator and adjust the parent accordingly |
| adjustDirtyStateRecursively(resource, indicator); |
| } |
| |
| /** |
| * Method getName. |
| * @param syncBytes |
| */ |
| private String getName(byte[] syncBytes) throws CVSException { |
| return ResourceSyncInfo.getName(syncBytes); |
| } |
| |
| /** |
| * Method isFolder. |
| * @param syncBytes |
| * @return boolean |
| */ |
| private boolean isFolder(byte[] syncBytes) { |
| return ResourceSyncInfo.isFolder(syncBytes); |
| } |
| |
| /** |
| * Method convertToDeletion. |
| * @param syncBytes |
| * @return byte[] |
| */ |
| private byte[] convertToDeletion(byte[] syncBytes) throws CVSException { |
| return ResourceSyncInfo.convertToDeletion(syncBytes); |
| } |
| |
| static public void debug(IResource resource, String indicator, String string) { |
| String di = EclipseSynchronizer.IS_DIRTY_INDICATOR; |
| if(indicator == EclipseSynchronizer.IS_DIRTY_INDICATOR) { |
| di = "dirty"; //$NON-NLS-1$ |
| } else if(indicator == EclipseSynchronizer.NOT_DIRTY_INDICATOR) { |
| di = "clean"; //$NON-NLS-1$ |
| } else { |
| di = "needs recomputing"; //$NON-NLS-1$ |
| } |
| System.out.println("["+string + ":" + di + "] " + resource.getFullPath()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| } |
| |
| /** |
| * @param file |
| * @return int |
| */ |
| public int getModificationState(IResource resource) throws CVSException { |
| String indicator = getDirtyIndicator(resource); |
| if (Policy.DEBUG_DIRTY_CACHING) { |
| debug(resource, indicator, "getModificationState"); //$NON-NLS-1$ |
| } |
| if (indicator == null || indicator == RECOMPUTE_INDICATOR) { |
| return ICVSFile.UNKNOWN; |
| } else if (indicator == IS_DIRTY_INDICATOR) { |
| return ICVSFile.DIRTY; |
| } else if (indicator == NOT_DIRTY_INDICATOR) { |
| return ICVSFile.CLEAN; |
| } else { |
| return ICVSFile.UNKNOWN; |
| } |
| } |
| |
| /** |
| * Return whether the resource is within the scope of a currently active |
| * CVS operation. |
| * @param resource |
| * @return |
| */ |
| public boolean isWithinActiveOperationScope(IResource resource) { |
| return resourceLock.isWithinActiveOperationScope(resource); |
| } |
| |
| /** |
| * Set the timestamp of the given file and set it to be CLEAN. It is |
| * assumed that this method is only invoked to reset the file timestamp |
| * to the timestamp that is in the CVS/Entries file. |
| * @param file |
| * @param time |
| * @throws CVSException |
| */ |
| public void setTimeStamp(EclipseFile cvsFile, long time) throws CVSException { |
| ISchedulingRule rule = null; |
| IFile file = (IFile)cvsFile.getIResource(); |
| try { |
| rule = beginBatching(file, null); |
| try { |
| beginOperation(); |
| try { |
| file.setLocalTimeStamp(time); |
| setModified(cvsFile, ICVSFile.CLEAN); |
| } catch (CoreException e) { |
| throw CVSException.wrapException(e); |
| } |
| resourceChanged(file); |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, null); |
| } |
| } |
| |
| /** |
| * React to a resource that was just moved by the move/delete hook. |
| * @param resource the resource that was moved (at its new location) |
| */ |
| public void postMove(IResource resource) throws CVSException { |
| try { |
| beginOperation(); |
| if (resource.getType() == IResource.FILE) { |
| // Purge any copied sync info so true sync info will |
| // be obtained from the synchronizer cache |
| sessionPropertyCache.purgeResourceSyncCache(resource); |
| } else { |
| IContainer container = (IContainer)resource; |
| // Purge any copied sync info |
| sessionPropertyCache.purgeCache(container, true /* deep */); |
| // Dirty all resources so old sync info will be rewritten to disk |
| try { |
| container.accept(new IResourceVisitor() { |
| public boolean visit(IResource resource) throws CoreException { |
| if (getSyncBytes(resource) != null) { |
| resourceChanged(resource); |
| } |
| if (resource.getType() != IResource.FILE) { |
| if (getFolderSync((IContainer)resource) != null) { |
| folderChanged((IContainer)resource); |
| return true; |
| } |
| } |
| return false; |
| } |
| }); |
| } catch (CoreException e) { |
| throw CVSException.wrapException(e); |
| } |
| // Flush the sync info to disk |
| flush(container, true /* deep */, null); |
| } |
| } finally { |
| endOperation(); |
| } |
| } |
| |
| /** |
| * This method is to be invoked only from the move/delete hook. Its purpose is to obtain the |
| * sync look in order to prevent other threads from accessing sync info while the move/delete is |
| * taking place. |
| * |
| * @param runnable |
| * @param monitor |
| * @throws CVSException |
| */ |
| public void performMoveDelete(ICVSRunnable runnable, IProgressMonitor monitor) throws CVSException { |
| ISchedulingRule rule = null; |
| try { |
| monitor.beginTask(null, 100); |
| rule = beginBatching(null, null); |
| try { |
| beginOperation(); |
| runnable.run(Policy.subMonitorFor(monitor, 95)); |
| } finally { |
| endOperation(); |
| } |
| } finally { |
| if (rule != null) endBatching(rule, Policy.subMonitorFor(monitor, 5)); |
| monitor.done(); |
| } |
| } |
| |
| /** |
| * Compute the modification state for the given file. If the modificationState is |
| * ICVSFile.UNKNOWN, it is computed. However, if it is CLEAN or DIRTY, |
| * it is set accordingly. CLEAN or DIRTY can only be used if the caller is protected |
| * from resource modifications (either by a scheduling rule or inside a delta handler). |
| * @param file |
| * @param modificationState |
| * @return true if the file is dirty |
| */ |
| public boolean setModified(EclipseFile cvsFile, int modificationState) throws CVSException { |
| try { |
| beginOperation(); |
| boolean dirty; |
| if (modificationState == ICVSFile.UNKNOWN) { |
| dirty = cvsFile.isDirty(); |
| } else { |
| dirty = modificationState == ICVSFile.DIRTY; |
| } |
| setDirtyIndicator(cvsFile.getIResource(), dirty); |
| return dirty; |
| } finally { |
| endOperation(); |
| } |
| |
| } |
| |
| /** |
| * Set the modified state of the folder. This method can be called when no resource locks are |
| * held. It will check the cached modification state of all the folder's children before setting. |
| * If the states of the children do not match, the state for the folder is not cached. |
| * @param folder |
| * @param modified |
| */ |
| public void setModified(ICVSFolder cvsFolder, boolean modified) throws CVSException { |
| try { |
| beginOperation(); |
| IContainer folder = (IContainer)cvsFolder.getIResource(); |
| // The drop out condition for clean or dirty are the opposite. |
| // (i.e. if modified and a dirty is found we can set the indicator |
| // and if not modified and a dirty or unknown is found we cannot set the indicator) |
| boolean okToSet = !modified; |
| // Obtain the children while we're locked to ensure some were not added or changed |
| ICVSResource[] children = cvsFolder.members(ICVSFolder.ALL_UNIGNORED_MEMBERS); |
| for (ICVSResource child : children) { |
| IResource resource = child.getIResource(); |
| if (modified) { |
| if (getDirtyIndicator(resource) == IS_DIRTY_INDICATOR) { |
| okToSet = true; |
| break; |
| } |
| } else { |
| if (getDirtyIndicator(resource) != NOT_DIRTY_INDICATOR) { |
| okToSet = false; |
| break; |
| } |
| } |
| } |
| if (okToSet) { |
| setDirtyIndicator(folder, modified); |
| } |
| } finally { |
| endOperation(); |
| } |
| } |
| |
| public boolean wasPhantom(IResource resource) { |
| if (resource.exists()) { |
| try { |
| return (synchronizerCache.getCachedSyncBytes(resource, true) != null |
| || (resource.getType() == IResource.FOLDER |
| && synchronizerCache.hasCachedFolderSync((IContainer)resource))); |
| } catch (CVSException e) { |
| // Log and assume resource is not a phantom |
| CVSProviderPlugin.log(e); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Method called from background handler when resources that are mapped to CVS are recreated |
| * @param resources |
| * @param monitor |
| * @throws CVSException |
| */ |
| public void resourcesRecreated(IResource[] resources, IProgressMonitor monitor) throws CVSException { |
| if (resources.length == 0) return; |
| ISchedulingRule rule = null; |
| ISchedulingRule projectsRule = getProjectRule(resources); |
| try { |
| monitor = Policy.monitorFor(monitor); |
| monitor.beginTask(null, 100); |
| rule = beginBatching(projectsRule, monitor); |
| for (IResource resource : resources) { |
| try { |
| created(resource); |
| } catch (CVSException e) { |
| CVSProviderPlugin.log(e); |
| } |
| } |
| } finally { |
| if (rule != null) endBatching(rule, Policy.subMonitorFor(monitor, 5)); |
| monitor.done(); |
| } |
| } |
| |
| private ISchedulingRule getProjectRule(IResource[] resources) { |
| HashSet<IProject> set = new HashSet<>(); |
| for (IResource resource : resources) { |
| set.add(resource.getProject()); |
| } |
| IProject[] projects = set.toArray(new IProject[set.size()]); |
| if (projects.length == 1) { |
| return projects[0]; |
| } |
| return new MultiRule(projects); |
| } |
| |
| protected void created(IResource resource) throws CVSException { |
| try { |
| beginOperation(); |
| if (resource.exists()) { |
| restoreResourceSync(resource); |
| if (resource.getType() == IResource.FOLDER) { |
| restoreFolderSync((IFolder)resource); |
| } |
| } |
| } finally { |
| endOperation(); |
| } |
| } |
| |
| /* |
| * Restore the folder sync info for the given folder |
| */ |
| private void restoreFolderSync(IFolder folder) throws CVSException { |
| try { |
| // set the dirty count using what was cached in the phantom it |
| beginOperation(); |
| FolderSyncInfo folderInfo = synchronizerCache.getCachedFolderSync(folder, true); |
| if (folderInfo != null) { |
| // There is folder sync info to restore |
| if (folder.getFolder(SyncFileWriter.CVS_DIRNAME).exists()) { |
| // There is already a CVS subdirectory which indicates that |
| // either the folder was recreated by an external tool or that |
| // a folder with CVS information was copied from another location. |
| // To know the difference, we need to compare the folder sync info. |
| // If they are mapped to the same root and repository then just |
| // purge the phantom info. Otherwise, keep the original sync info. |
| |
| // Get the new folder sync info |
| FolderSyncInfo newFolderInfo = getFolderSync(folder); |
| if (newFolderInfo.getRoot().equals(folderInfo.getRoot()) |
| && newFolderInfo.getRepository().equals(folderInfo.getRepository())) { |
| // The folder is the same so use what is on disk. |
| // Fall through to ensure that the Root and Repository files exist |
| } else { |
| // The folder is mapped to a different location. |
| // Purge new resource sync before restoring from phantom |
| ICVSFolder cvsFolder = CVSWorkspaceRoot.getCVSFolderFor(folder); |
| ICVSResource[] children = cvsFolder.members(ICVSFolder.MANAGED_MEMBERS); |
| for (ICVSResource resource : children) { |
| deleteResourceSync(resource.getIResource()); |
| } |
| } |
| } |
| |
| // set the sync info using what was cached in the phantom |
| setFolderSync(folder, folderInfo); |
| // purge the dirty cache so any old persisted dirty state is purged |
| sessionPropertyCache.purgeDirtyCache(folder); |
| // Indicate that a member has changed so the entries file gets written (see bug 181546) |
| IResource[] members = members(folder); |
| IResource changedResource = null; |
| for (IResource resource : members) { |
| if (getSyncBytes(resource) != null) { |
| changedResource = resource; |
| break; |
| } |
| } |
| if (changedResource == null) { |
| changedResource = folder.getFile("dummy"); //$NON-NLS-1$ |
| } |
| resourceChanged(changedResource); |
| } |
| } finally { |
| try { |
| endOperation(); |
| } finally { |
| synchronizerCache.flush(folder); |
| } |
| } |
| } |
| |
| /* |
| * Restore the resource sync info for the given resource. |
| */ |
| private void restoreResourceSync(IResource resource) throws CVSException { |
| try { |
| beginOperation(); |
| byte[] syncBytes = synchronizerCache.getCachedSyncBytes(resource, true); |
| if (syncBytes != null) { |
| if (!ResourceSyncInfo.isFolder(syncBytes)) { |
| syncBytes = ResourceSyncInfo.convertFromDeletion(syncBytes); |
| } |
| byte[] newBytes = getSyncBytes(resource); |
| if (newBytes != null && !ResourceSyncInfo.isFolder(newBytes)) { |
| newBytes = ResourceSyncInfo.convertFromDeletion(newBytes); |
| } |
| if (newBytes == null || Util.equals(syncBytes, newBytes)) { |
| // only move the sync info if there is no new sync info |
| setSyncBytes(resource, syncBytes); |
| } |
| } |
| } finally { |
| try { |
| endOperation(); |
| } finally { |
| synchronizerCache.setCachedSyncBytes(resource, null, true); |
| } |
| } |
| } |
| |
| private void purgeDirtyCache(IProject project, IProgressMonitor monitor) throws CVSException { |
| sessionPropertyCache.purgeDirtyCache(project); |
| } |
| } |