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