001 /*******************************************************************************
002 * Copyright (c) 2000, 2004 IBM Corporation and others.
003 * All rights reserved. This program and the accompanying materials
004 * are made available under the terms of the Eclipse Public License v1.0
005 * which accompanies this distribution, and is available at
006 * http://www.eclipse.org/legal/epl-v10.html
007 *
008 * Contributors:
009 * IBM Corporation - initial API and implementation
010 *******************************************************************************/
011 package org.eclipse.team.internal.ccvs.core;
012
013 import java.util.*;
014 import org.eclipse.core.resources.*;
015 import org.eclipse.core.runtime.*;
016 import org.eclipse.osgi.util.NLS;
017 import org.eclipse.team.core.RepositoryProvider;
018 import org.eclipse.team.core.TeamException;
019 import org.eclipse.team.core.subscribers.*;
020 import org.eclipse.team.core.synchronize.SyncInfo;
021 import org.eclipse.team.core.synchronize.SyncInfoFilter;
022 import org.eclipse.team.core.variants.*;
023 import org.eclipse.team.internal.ccvs.core.resources.CVSWorkspaceRoot;
024 import org.eclipse.team.internal.ccvs.core.resources.RemoteFile;
025 import org.eclipse.team.internal.ccvs.core.syncinfo.CVSResourceVariantTree;
026 import org.eclipse.team.internal.ccvs.core.syncinfo.ResourceSyncInfo;
027 import org.eclipse.team.internal.ccvs.core.util.Util;
028
029 /**
030 * A CVSMergeSubscriber is responsible for maintaining the remote trees for a merge into
031 * the workspace. The remote trees represent the CVS revisions of the start and end
032 * points (version or branch) of the merge.
033 *
034 * This subscriber stores the remote handles in the resource tree sync info slot. When
035 * the merge is cancelled this sync info is cleared.
036 *
037 * A merge can persist between workbench sessions and thus can be used as an
038 * ongoing merge.
039 *
040 * TODO: Is the merge subscriber interested in workspace sync info changes?
041 * TODO: Do certain operations (e.g. replace with) invalidate a merge subscriber?
042 * TODO: How to ensure that sync info is flushed when merge roots are deleted?
043 */
044 public class CVSMergeSubscriber extends CVSSyncTreeSubscriber implements IResourceChangeListener, ISubscriberChangeListener {
045
046 private final class MergeBaseTree extends CVSResourceVariantTree {
047 // The merge synchronizer has been kept so that those upgrading
048 // from 3.0 M8 to 3.0 M9 so not lose there ongoing merge state
049 private PersistantResourceVariantByteStore mergedSynchronizer;
050 private MergeBaseTree(ResourceVariantByteStore cache, CVSTag tag, boolean cacheFileContentsHint, String syncKeyPrefix) {
051 super(cache, tag, cacheFileContentsHint);
052 mergedSynchronizer = new PersistantResourceVariantByteStore(new QualifiedName(SYNC_KEY_QUALIFIER, syncKeyPrefix + "0merged")); //$NON-NLS-1$
053 }
054 public IResource[] refresh(IResource[] resources, int depth, IProgressMonitor monitor) throws TeamException {
055 // Only refresh the base of a resource once as it should not change
056 List unrefreshed = new ArrayList();
057 for (int i = 0; i < resources.length; i++) {
058 IResource resource = resources[i];
059 if (!hasResourceVariant(resource)) {
060 unrefreshed.add(resource);
061 }
062 }
063 if (unrefreshed.isEmpty()) {
064 monitor.done();
065 return new IResource[0];
066 }
067 IResource[] refreshed = super.refresh((IResource[]) unrefreshed.toArray(new IResource[unrefreshed.size()]), depth, monitor);
068 return refreshed;
069 }
070 public IResourceVariant getResourceVariant(IResource resource) throws TeamException {
071 // Use the merged bytes for the base if there are some
072 byte[] mergedBytes = mergedSynchronizer.getBytes(resource);
073 if (mergedBytes != null) {
074 byte[] parentBytes = getByteStore().getBytes(resource.getParent());
075 if (parentBytes != null) {
076 return RemoteFile.fromBytes(resource, mergedBytes, parentBytes);
077 }
078 }
079 return super.getResourceVariant(resource);
080 }
081
082 /**
083 * Mark the resource as merged by making it's base equal the remote
084 */
085 public void merged(IResource resource, byte[] remoteBytes) throws TeamException {
086 if (remoteBytes == null) {
087 getByteStore().deleteBytes(resource);
088 } else {
089 getByteStore().setBytes(resource, remoteBytes);
090 }
091 }
092
093 /**
094 * Return true if the remote has already been merged
095 * (i.e. the base equals the remote).
096 */
097 public boolean isMerged(IResource resource, byte[] remoteBytes) throws TeamException {
098 byte[] mergedBytes = getByteStore().getBytes(resource);
099 return Util.equals(mergedBytes, remoteBytes);
100 }
101
102 /* (non-Javadoc)
103 * @see org.eclipse.team.internal.ccvs.core.syncinfo.CVSResourceVariantTree#dispose()
104 */
105 public void dispose() {
106 mergedSynchronizer.dispose();
107 super.dispose();
108 }
109 }
110
111 public static final String ID = "org.eclipse.team.cvs.ui.cvsmerge-participant"; //$NON-NLS-1$
112 public static final String ID_MODAL = "org.eclipse.team.cvs.ui.cvsmerge-participant-modal"; //$NON-NLS-1$
113 private static final String UNIQUE_ID_PREFIX = "merge-"; //$NON-NLS-1$
114
115 private CVSTag start, end;
116 private List roots;
117 private CVSResourceVariantTree remoteTree;
118 private MergeBaseTree baseTree;
119
120 public CVSMergeSubscriber(IResource[] roots, CVSTag start, CVSTag end) {
121 this(getUniqueId(), roots, start, end);
122 }
123
124 private static QualifiedName getUniqueId() {
125 String uniqueId = Long.toString(System.currentTimeMillis());
126 return new QualifiedName(ID, "CVS" + UNIQUE_ID_PREFIX + uniqueId); //$NON-NLS-1$
127 }
128
129 public CVSMergeSubscriber(QualifiedName id, IResource[] roots, CVSTag start, CVSTag end) {
130 super(id, NLS.bind(CVSMessages.CVSMergeSubscriber_2, new String[] { start.getName(), end.getName() })); //$NON-NLS-1$ //$NON-NLS-2$
131 this.start = start;
132 this.end = end;
133 this.roots = new ArrayList(Arrays.asList(roots));
134 initialize();
135 }
136
137 /* (non-Javadoc)
138 * @see org.eclipse.team.internal.ccvs.core.CVSWorkspaceSubscriber#initialize()
139 */
140 private void initialize() {
141 QualifiedName id = getId();
142 String syncKeyPrefix = id.getLocalName();
143 PersistantResourceVariantByteStore remoteSynchronizer = new PersistantResourceVariantByteStore(new QualifiedName(SYNC_KEY_QUALIFIER, syncKeyPrefix + end.getName()));
144 remoteTree = new CVSResourceVariantTree(remoteSynchronizer, getEndTag(), getCacheFileContentsHint()) {
145 public IResource[] refresh(IResource[] resources, int depth, IProgressMonitor monitor) throws TeamException {
146 // Override refresh to compare file contents
147 monitor.beginTask(null, 100);
148 try {
149 IResource[] refreshed = super.refresh(resources, depth, monitor);
150 compareWithRemote(refreshed, Policy.subMonitorFor(monitor, 50));
151 return refreshed;
152 } finally {
153 monitor.done();
154 }
155 }
156 };
157 PersistantResourceVariantByteStore baseSynchronizer = new PersistantResourceVariantByteStore(new QualifiedName(SYNC_KEY_QUALIFIER, syncKeyPrefix + start.getName()));
158 baseTree = new MergeBaseTree(baseSynchronizer, getStartTag(), getCacheFileContentsHint(), syncKeyPrefix);
159
160 ResourcesPlugin.getWorkspace().addResourceChangeListener(this);
161 CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber().addListener(this);
162 }
163
164 protected SyncInfo getSyncInfo(IResource local, IResourceVariant base, IResourceVariant remote) throws TeamException {
165 CVSMergeSyncInfo info = new CVSMergeSyncInfo(local, base, remote, this);
166 info.init();
167 return info;
168 }
169
170 public void merged(IResource[] resources) throws TeamException {
171 for (int i = 0; i < resources.length; i++) {
172 IResource resource = resources[i];
173 internalMerged(resource);
174 }
175 fireTeamResourceChange(SubscriberChangeEvent.asSyncChangedDeltas(this, resources));
176 }
177
178 private void internalMerged(IResource resource) throws TeamException {
179 byte[] remoteBytes = getRemoteByteStore().getBytes(resource);
180 baseTree.merged(resource, remoteBytes);
181 }
182
183 /* (non-Javadoc)
184 * @see org.eclipse.team.core.sync.TeamSubscriber#cancel()
185 */
186 public void cancel() {
187 ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
188 remoteTree.dispose();
189 baseTree.dispose();
190 }
191
192 /* (non-Javadoc)
193 * @see org.eclipse.team.core.sync.TeamSubscriber#roots()
194 */
195 public IResource[] roots() {
196 return (IResource[]) roots.toArray(new IResource[roots.size()]);
197 }
198
199 /* (non-Javadoc)
200 * @see org.eclipse.team.core.sync.TeamSubscriber#isSupervised(org.eclipse.core.resources.IResource)
201 */
202 public boolean isSupervised(IResource resource) throws TeamException {
203 return getBaseTree().hasResourceVariant(resource) || getRemoteTree().hasResourceVariant(resource);
204 }
205
206 public CVSTag getStartTag() {
207 return start;
208 }
209
210 public CVSTag getEndTag() {
211 return end;
212 }
213
214 /*
215 * What to do when a root resource for this merge changes?
216 * Deleted, Move, Copied
217 * Changed in a CVS way (tag changed, revision changed...)
218 * Contents changed by user
219 * @see IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
220 */
221 public void resourceChanged(IResourceChangeEvent event) {
222 try {
223 IResourceDelta delta = event.getDelta();
224 if(delta != null) {
225 delta.accept(new IResourceDeltaVisitor() {
226 public boolean visit(IResourceDelta delta) throws CoreException {
227 IResource resource = delta.getResource();
228
229 if (resource.getType()==IResource.PROJECT) {
230 IProject project = (IProject)resource;
231 if (!project.isAccessible()) {
232 return false;
233 }
234 if ((delta.getFlags() & IResourceDelta.OPEN) != 0) {
235 return false;
236 }
237 if (RepositoryProvider.getProvider(project, CVSProviderPlugin.getTypeId()) == null) {
238 return false;
239 }
240 }
241
242 if (roots.contains(resource)) {
243 if (delta.getKind() == IResourceDelta.REMOVED || delta.getKind() == IResourceDelta.MOVED_TO) {
244 cancel();
245 }
246 // stop visiting children
247 return false;
248 }
249 // keep visiting children
250 return true;
251 }
252 });
253 }
254 } catch (CoreException e) {
255 CVSProviderPlugin.log(e.getStatus());
256 }
257 }
258
259 /**
260 * Return whether the given resource has been merged with its
261 * corresponding remote.
262 * @param resource the local resource
263 * @return boolean
264 * @throws TeamException
265 */
266 public boolean isMerged(IResource resource) throws TeamException {
267 byte[] remoteBytes = getRemoteByteStore().getBytes(resource);
268 return baseTree.isMerged(resource, remoteBytes);
269 }
270
271 /*
272 * Currently only the workspace subscriber knows when a project has been deconfigured. We will listen for these events
273 * and remove the root then forward to merge subscriber listeners.
274 * (non-Javadoc)
275 * @see org.eclipse.team.core.subscribers.ITeamResourceChangeListener#teamResourceChanged(org.eclipse.team.core.subscribers.TeamDelta[])
276 */
277 public void subscriberResourceChanged(ISubscriberChangeEvent[] deltas) {
278 for (int i = 0; i < deltas.length; i++) {
279 ISubscriberChangeEvent delta = deltas[i];
280 switch(delta.getFlags()) {
281 case ISubscriberChangeEvent.ROOT_REMOVED:
282 IResource resource = delta.getResource();
283 if(roots.remove(resource)) {
284 fireTeamResourceChange(new ISubscriberChangeEvent[] {delta});
285 }
286 break;
287 }
288 }
289 }
290
291 /* (non-Javadoc)
292 * @see org.eclipse.team.internal.ccvs.core.CVSSyncTreeSubscriber#getBaseSynchronizationCache()
293 */
294 protected IResourceVariantTree getBaseTree() {
295 return baseTree;
296 }
297
298 /* (non-Javadoc)
299 * @see org.eclipse.team.internal.ccvs.core.CVSSyncTreeSubscriber#getRemoteSynchronizationCache()
300 */
301 protected IResourceVariantTree getRemoteTree() {
302 return remoteTree;
303 }
304
305 protected boolean getCacheFileContentsHint() {
306 return true;
307 }
308
309 /*
310 * Mark as merged any local resources whose contents match that of the remote resource.
311 */
312 private void compareWithRemote(IResource[] refreshed, IProgressMonitor monitor) throws CVSException, TeamException {
313 // For any remote changes, if the revision differs from the local, compare the contents.
314 if (refreshed.length == 0) return;
315 SyncInfoFilter.ContentComparisonSyncInfoFilter contentFilter =
316 new SyncInfoFilter.ContentComparisonSyncInfoFilter();
317 monitor.beginTask(null, refreshed.length * 100);
318 for (int i = 0; i < refreshed.length; i++) {
319 IResource resource = refreshed[i];
320 if (resource.getType() == IResource.FILE) {
321 ICVSFile local = CVSWorkspaceRoot.getCVSFileFor((IFile)resource);
322 byte[] localBytes = local.getSyncBytes();
323 byte[] remoteBytes = getRemoteByteStore().getBytes(resource);
324 if (remoteBytes != null
325 && localBytes != null
326 && local.exists()
327 && !ResourceSyncInfo.getRevision(remoteBytes).equals(ResourceSyncInfo.getRevision(localBytes))
328 && contentFilter.select(getSyncInfo(resource), Policy.subMonitorFor(monitor, 100))) {
329 // The contents are equals so mark the file as merged
330 internalMerged(resource);
331 }
332 }
333 }
334 monitor.done();
335 }
336
337
338 private PersistantResourceVariantByteStore getRemoteByteStore() {
339 return (PersistantResourceVariantByteStore)((CVSResourceVariantTree)getRemoteTree()).getByteStore();
340 }
341
342 /* (non-Javadoc)
343 * @see java.lang.Object#equals(java.lang.Object)
344 */
345 public boolean equals(Object other) {
346 if(this == other) return true;
347 if(! (other instanceof CVSMergeSubscriber)) return false;
348 CVSMergeSubscriber s = (CVSMergeSubscriber)other;
349 return getEndTag().equals(s.getEndTag()) &&
350 getStartTag().equals(s.getStartTag()) && rootsEqual(s);
351 }
352 }