1 /******************************************************************************* 2 * Copyright (c) 2000, 2013 IBM Corporation and others. 3 * 4 * This program and the accompanying materials 5 * are made available under the terms of the Eclipse Public License 2.0 6 * which accompanies this distribution, and is available at 7 * https://www.eclipse.org/legal/epl-2.0/ 8 * 9 * SPDX-License-Identifier: EPL-2.0 10 * 11 * Contributors: 12 * IBM Corporation - initial API and implementation 13 *******************************************************************************/ 14 package org.eclipse.team.internal.ccvs.core; 15 16 import java.util.*; 17 18 import org.eclipse.core.resources.*; 19 import org.eclipse.core.runtime.*; 20 import org.eclipse.osgi.util.NLS; 21 import org.eclipse.team.core.RepositoryProvider; 22 import org.eclipse.team.core.TeamException; 23 import org.eclipse.team.core.subscribers.*; 24 import org.eclipse.team.core.synchronize.SyncInfo; 25 import org.eclipse.team.core.synchronize.SyncInfoFilter; 26 import org.eclipse.team.core.variants.*; 27 import org.eclipse.team.internal.ccvs.core.resources.CVSWorkspaceRoot; 28 import org.eclipse.team.internal.ccvs.core.resources.RemoteFile; 29 import org.eclipse.team.internal.ccvs.core.syncinfo.CVSResourceVariantTree; 30 import org.eclipse.team.internal.ccvs.core.syncinfo.ResourceSyncInfo; 31 import org.eclipse.team.internal.ccvs.core.util.Util; 32 33 /** 34 * A CVSMergeSubscriber is responsible for maintaining the remote trees for a merge into 35 * the workspace. The remote trees represent the CVS revisions of the start and end 36 * points (version or branch) of the merge. 37 * 38 * This subscriber stores the remote handles in the resource tree sync info slot. When 39 * the merge is cancelled this sync info is cleared. 40 * 41 * A merge can persist between workbench sessions and thus can be used as an 42 * ongoing merge. 43 * 44 * TODO: Is the merge subscriber interested in workspace sync info changes? 45 * TODO: Do certain operations (e.g. replace with) invalidate a merge subscriber? 46 * TODO: How to ensure that sync info is flushed when merge roots are deleted? 47 */ 48 public class CVSMergeSubscriber extends CVSSyncTreeSubscriber implements IResourceChangeListener, ISubscriberChangeListener { 49 50 private final class MergeBaseTree extends CVSResourceVariantTree { 51 // The merge synchronizer has been kept so that those upgrading 52 // from 3.0 M8 to 3.0 M9 so not lose there ongoing merge state 53 private PersistantResourceVariantByteStore mergedSynchronizer; MergeBaseTree(ResourceVariantByteStore cache, CVSTag tag, boolean cacheFileContentsHint, String syncKeyPrefix)54 private MergeBaseTree(ResourceVariantByteStore cache, CVSTag tag, boolean cacheFileContentsHint, String syncKeyPrefix) { 55 super(cache, tag, cacheFileContentsHint); 56 mergedSynchronizer = new PersistantResourceVariantByteStore(new QualifiedName(SYNC_KEY_QUALIFIER, syncKeyPrefix + "0merged")); //$NON-NLS-1$ 57 } refresh(IResource[] resources, int depth, IProgressMonitor monitor)58 public IResource[] refresh(IResource[] resources, int depth, IProgressMonitor monitor) throws TeamException { 59 // Only refresh the base of a resource once as it should not change 60 List unrefreshed = new ArrayList(); 61 for (IResource resource : resources) { 62 if (!hasResourceVariant(resource)) { 63 unrefreshed.add(resource); 64 } 65 } 66 if (unrefreshed.isEmpty()) { 67 monitor.done(); 68 return new IResource[0]; 69 } 70 IResource[] refreshed = super.refresh((IResource[]) unrefreshed.toArray(new IResource[unrefreshed.size()]), depth, monitor); 71 return refreshed; 72 } getResourceVariant(IResource resource)73 public IResourceVariant getResourceVariant(IResource resource) throws TeamException { 74 // Use the merged bytes for the base if there are some 75 byte[] mergedBytes = mergedSynchronizer.getBytes(resource); 76 if (mergedBytes != null) { 77 byte[] parentBytes = getByteStore().getBytes(resource.getParent()); 78 if (parentBytes != null) { 79 return RemoteFile.fromBytes(resource, mergedBytes, parentBytes); 80 } 81 } 82 return super.getResourceVariant(resource); 83 } 84 85 /** 86 * Mark the resource as merged by making it's base equal the remote 87 */ merged(IResource resource, byte[] remoteBytes)88 public void merged(IResource resource, byte[] remoteBytes) throws TeamException { 89 if (remoteBytes == null) { 90 getByteStore().deleteBytes(resource); 91 } else { 92 getByteStore().setBytes(resource, remoteBytes); 93 } 94 } 95 96 /** 97 * Return true if the remote has already been merged 98 * (i.e. the base equals the remote). 99 */ isMerged(IResource resource, byte[] remoteBytes)100 public boolean isMerged(IResource resource, byte[] remoteBytes) throws TeamException { 101 byte[] mergedBytes = getByteStore().getBytes(resource); 102 return Util.equals(mergedBytes, remoteBytes); 103 } 104 105 @Override dispose()106 public void dispose() { 107 mergedSynchronizer.dispose(); 108 super.dispose(); 109 } 110 } 111 112 public static final String ID = "org.eclipse.team.cvs.ui.cvsmerge-participant"; //$NON-NLS-1$ 113 public static final String ID_MODAL = "org.eclipse.team.cvs.ui.cvsmerge-participant-modal"; //$NON-NLS-1$ 114 private static final String UNIQUE_ID_PREFIX = "merge-"; //$NON-NLS-1$ 115 116 private CVSTag start, end; 117 private List<IResource> roots; 118 private CVSResourceVariantTree remoteTree; 119 private MergeBaseTree baseTree; 120 private boolean isModelSync; 121 CVSMergeSubscriber(IResource[] roots, CVSTag start, CVSTag end, boolean isModelSync)122 public CVSMergeSubscriber(IResource[] roots, CVSTag start, CVSTag end, boolean isModelSync) { 123 this(getUniqueId(), roots, start, end); 124 this.isModelSync = isModelSync; 125 } 126 getUniqueId()127 private static QualifiedName getUniqueId() { 128 String uniqueId = Long.toString(System.currentTimeMillis()); 129 return new QualifiedName(ID, "CVS" + UNIQUE_ID_PREFIX + uniqueId); //$NON-NLS-1$ 130 } 131 CVSMergeSubscriber(QualifiedName id, IResource[] roots, CVSTag start, CVSTag end)132 public CVSMergeSubscriber(QualifiedName id, IResource[] roots, CVSTag start, CVSTag end) { 133 super(id, NLS.bind(CVSMessages.CVSMergeSubscriber_2, new String[] { start.getName(), end.getName() })); 134 this.start = start; 135 this.end = end; 136 this.roots = new ArrayList<>(Arrays.asList(roots)); 137 initialize(); 138 } 139 140 /* 141 * @see org.eclipse.team.internal.ccvs.core.CVSWorkspaceSubscriber#initialize() 142 */ initialize()143 private void initialize() { 144 QualifiedName id = getId(); 145 String syncKeyPrefix = id.getLocalName(); 146 PersistantResourceVariantByteStore remoteSynchronizer = new PersistantResourceVariantByteStore(new QualifiedName(SYNC_KEY_QUALIFIER, syncKeyPrefix + end.getName())); 147 remoteTree = new CVSResourceVariantTree(remoteSynchronizer, getEndTag(), getCacheFileContentsHint()) { 148 public IResource[] refresh(IResource[] resources, int depth, IProgressMonitor monitor) throws TeamException { 149 // Override refresh to compare file contents 150 monitor.beginTask(null, 100); 151 try { 152 IResource[] refreshed = super.refresh(resources, depth, monitor); 153 compareWithRemote(refreshed, Policy.subMonitorFor(monitor, 50)); 154 return refreshed; 155 } finally { 156 monitor.done(); 157 } 158 } 159 }; 160 PersistantResourceVariantByteStore baseSynchronizer = new PersistantResourceVariantByteStore(new QualifiedName(SYNC_KEY_QUALIFIER, syncKeyPrefix + start.getName())); 161 baseTree = new MergeBaseTree(baseSynchronizer, getStartTag(), getCacheFileContentsHint(), syncKeyPrefix); 162 163 ResourcesPlugin.getWorkspace().addResourceChangeListener(this); 164 CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber().addListener(this); 165 } 166 getSyncInfo(IResource local, IResourceVariant base, IResourceVariant remote)167 protected SyncInfo getSyncInfo(IResource local, IResourceVariant base, IResourceVariant remote) throws TeamException { 168 CVSMergeSyncInfo info = new CVSMergeSyncInfo(local, base, remote, this); 169 info.init(); 170 return info; 171 } 172 merged(IResource[] resources)173 public void merged(IResource[] resources) throws TeamException { 174 for (IResource resource : resources) { 175 internalMerged(resource); 176 } 177 fireTeamResourceChange(SubscriberChangeEvent.asSyncChangedDeltas(this, resources)); 178 } 179 internalMerged(IResource resource)180 private void internalMerged(IResource resource) throws TeamException { 181 byte[] remoteBytes = getRemoteByteStore().getBytes(resource); 182 baseTree.merged(resource, remoteBytes); 183 } 184 185 /* 186 * @see org.eclipse.team.core.sync.TeamSubscriber#cancel() 187 */ cancel()188 public void cancel() { 189 ResourcesPlugin.getWorkspace().removeResourceChangeListener(this); 190 remoteTree.dispose(); 191 baseTree.dispose(); 192 } 193 194 @Override roots()195 public IResource[] roots() { 196 return roots.toArray(new IResource[roots.size()]); 197 } 198 199 @Override isSupervised(IResource resource)200 public boolean isSupervised(IResource resource) throws TeamException { 201 return getBaseTree().hasResourceVariant(resource) || getRemoteTree().hasResourceVariant(resource); 202 } 203 getStartTag()204 public CVSTag getStartTag() { 205 return start; 206 } 207 getEndTag()208 public CVSTag getEndTag() { 209 return end; 210 } 211 isModelSync()212 boolean isModelSync() { 213 return isModelSync; 214 } 215 216 /* 217 * What to do when a root resource for this merge changes? 218 * Deleted, Move, Copied 219 * Changed in a CVS way (tag changed, revision changed...) 220 * Contents changed by user 221 * @see IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent) 222 */ resourceChanged(IResourceChangeEvent event)223 public void resourceChanged(IResourceChangeEvent event) { 224 try { 225 IResourceDelta delta = event.getDelta(); 226 if(delta != null) { 227 delta.accept(new IResourceDeltaVisitor() { 228 public boolean visit(IResourceDelta delta) throws CoreException { 229 IResource resource = delta.getResource(); 230 231 if (resource.getType()==IResource.PROJECT) { 232 IProject project = (IProject)resource; 233 if (!project.isAccessible()) { 234 return false; 235 } 236 if ((delta.getFlags() & IResourceDelta.OPEN) != 0) { 237 return false; 238 } 239 if (RepositoryProvider.getProvider(project, CVSProviderPlugin.getTypeId()) == null) { 240 return false; 241 } 242 } 243 244 if (roots.contains(resource)) { 245 if (delta.getKind() == IResourceDelta.REMOVED || delta.getKind() == IResourceDelta.MOVED_TO) { 246 cancel(); 247 } 248 // stop visiting children 249 return false; 250 } 251 // keep visiting children 252 return true; 253 } 254 }); 255 } 256 } catch (CoreException e) { 257 CVSProviderPlugin.log(e.getStatus()); 258 } 259 } 260 261 /** 262 * Return whether the given resource has been merged with its 263 * corresponding remote. 264 * @param resource the local resource 265 * @return boolean 266 * @throws TeamException 267 */ isMerged(IResource resource)268 public boolean isMerged(IResource resource) throws TeamException { 269 byte[] remoteBytes = getRemoteByteStore().getBytes(resource); 270 return baseTree.isMerged(resource, remoteBytes); 271 } 272 273 /* 274 * Currently only the workspace subscriber knows when a project has been deconfigured. We will listen for these events 275 * and remove the root then forward to merge subscriber listeners. 276 * 277 * @see org.eclipse.team.core.subscribers.ITeamResourceChangeListener#teamResourceChanged(org.eclipse.team.core.subscribers.TeamDelta[]) 278 */ subscriberResourceChanged(ISubscriberChangeEvent[] deltas)279 public void subscriberResourceChanged(ISubscriberChangeEvent[] deltas) { 280 for (ISubscriberChangeEvent delta : deltas) { 281 switch(delta.getFlags()) { 282 case ISubscriberChangeEvent.ROOT_REMOVED: 283 IResource resource = delta.getResource(); 284 if(roots.remove(resource)) { 285 fireTeamResourceChange(new ISubscriberChangeEvent[] {delta}); 286 } 287 break; 288 default: 289 break; 290 } 291 } 292 } 293 294 @Override getBaseTree()295 protected IResourceVariantTree getBaseTree() { 296 return baseTree; 297 } 298 299 @Override getRemoteTree()300 protected IResourceVariantTree getRemoteTree() { 301 return remoteTree; 302 } 303 getCacheFileContentsHint()304 protected boolean getCacheFileContentsHint() { 305 return true; 306 } 307 308 /* 309 * Mark as merged any local resources whose contents match that of the remote resource. 310 */ compareWithRemote(IResource[] refreshed, IProgressMonitor monitor)311 private void compareWithRemote(IResource[] refreshed, IProgressMonitor monitor) throws CVSException, TeamException { 312 // For any remote changes, if the revision differs from the local, compare the contents. 313 if (refreshed.length == 0) return; 314 SyncInfoFilter.ContentComparisonSyncInfoFilter contentFilter = 315 new SyncInfoFilter.ContentComparisonSyncInfoFilter(); 316 monitor.beginTask(null, refreshed.length * 100); 317 for (IResource resource : refreshed) { 318 if (resource.getType() == IResource.FILE) { 319 ICVSFile local = CVSWorkspaceRoot.getCVSFileFor((IFile)resource); 320 byte[] localBytes = local.getSyncBytes(); 321 byte[] remoteBytes = getRemoteByteStore().getBytes(resource); 322 if (remoteBytes != null 323 && localBytes != null 324 && local.exists() 325 && !ResourceSyncInfo.getRevision(remoteBytes).equals(ResourceSyncInfo.getRevision(localBytes)) 326 && contentFilter.select(getSyncInfo(resource), Policy.subMonitorFor(monitor, 100))) { 327 // The contents are equals so mark the file as merged 328 internalMerged(resource); 329 } 330 } 331 } 332 monitor.done(); 333 } 334 335 getRemoteByteStore()336 private PersistantResourceVariantByteStore getRemoteByteStore() { 337 return (PersistantResourceVariantByteStore)((CVSResourceVariantTree)getRemoteTree()).getByteStore(); 338 } 339 340 @Override equals(Object other)341 public boolean equals(Object other) { 342 if(this == other) return true; 343 if(! (other instanceof CVSMergeSubscriber)) return false; 344 CVSMergeSubscriber s = (CVSMergeSubscriber)other; 345 return getEndTag().equals(s.getEndTag()) && 346 getStartTag().equals(s.getStartTag()) && rootsEqual(s); 347 } 348 } 349