1 /*******************************************************************************
2  * Copyright (c) 2000, 2010 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.ui.subscriber;
15 
16 import java.lang.reflect.InvocationTargetException;
17 import java.util.*;
18 
19 import org.eclipse.compare.structuremergeviewer.IDiffElement;
20 import org.eclipse.core.resources.*;
21 import org.eclipse.core.resources.mapping.ResourceMapping;
22 import org.eclipse.core.resources.mapping.ResourceMappingContext;
23 import org.eclipse.core.runtime.CoreException;
24 import org.eclipse.core.runtime.IProgressMonitor;
25 import org.eclipse.core.runtime.jobs.ISchedulingRule;
26 import org.eclipse.core.runtime.jobs.MultiRule;
27 import org.eclipse.jface.dialogs.MessageDialog;
28 import org.eclipse.osgi.util.NLS;
29 import org.eclipse.team.core.TeamException;
30 import org.eclipse.team.core.mapping.provider.SynchronizationScopeManager;
31 import org.eclipse.team.core.synchronize.*;
32 import org.eclipse.team.core.synchronize.FastSyncInfoFilter.*;
33 import org.eclipse.team.core.variants.IResourceVariant;
34 import org.eclipse.team.internal.ccvs.core.*;
35 import org.eclipse.team.internal.ccvs.core.client.Command.LocalOption;
36 import org.eclipse.team.internal.ccvs.core.resources.CVSWorkspaceRoot;
37 import org.eclipse.team.internal.ccvs.core.resources.EclipseSynchronizer;
38 import org.eclipse.team.internal.ccvs.core.syncinfo.ResourceSyncInfo;
39 import org.eclipse.team.internal.ccvs.ui.CVSUIMessages;
40 import org.eclipse.team.internal.ccvs.ui.CVSUIPlugin;
41 import org.eclipse.team.internal.ccvs.ui.Policy;
42 import org.eclipse.team.internal.ccvs.ui.operations.*;
43 import org.eclipse.team.internal.ui.TeamUIPlugin;
44 import org.eclipse.team.internal.ui.Utils;
45 import org.eclipse.team.ui.synchronize.ISynchronizePageConfiguration;
46 
47 /**
48  * This update action will update all mergable resources first and then prompt the
49  * user to overwrite any resources that failed the safe update.
50  *
51  * Subclasses should determine how the update should handle conflicts by implementing
52  * the getOverwriteLocalChanges() method.
53  */
54 public abstract class SafeUpdateOperation extends CVSSubscriberOperation {
55 
56 	private boolean promptBeforeUpdate = false;
57 
58 	private SyncInfoSet skipped = new SyncInfoSet();
59 
SafeUpdateOperation(ISynchronizePageConfiguration configuration, IDiffElement[] elements, boolean promptBeforeUpdate)60 	protected SafeUpdateOperation(ISynchronizePageConfiguration configuration, IDiffElement[] elements, boolean promptBeforeUpdate) {
61 		super(configuration, elements);
62 		this.promptBeforeUpdate = promptBeforeUpdate;
63 	}
64 
65 	@Override
shouldRun()66 	public boolean shouldRun() {
67 		return promptIfNeeded();
68 	}
69 
70 	/**
71 	 * Run the operation for the sync infos from the given project.
72 	 *
73 	 * @param projectSyncInfos the project syncInfos
74 	 * @param project the project
75 	 * @param monitor a progress monitor
76 	 * @throws InvocationTargetException
77 	 */
78 	@Override
run(final Map projectSyncInfos, final IProject project, IProgressMonitor monitor)79 	protected void run(final Map projectSyncInfos, final IProject project,
80 			IProgressMonitor monitor) throws InvocationTargetException {
81 		try {
82 			IResource[] resources = getIResourcesFrom(((SyncInfoSet) projectSyncInfos
83 					.get(project)).getSyncInfos());
84 			ResourceMapping[] selectedMappings = Utils
85 					.getResourceMappings(resources);
86 			ResourceMappingContext context = new SingleProjectSubscriberContext(
87 					CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber(),
88 					false, project);
89 			SynchronizationScopeManager manager = new SingleProjectScopeManager(
90 					getJobName(), selectedMappings, context, true, project);
91 			manager.initialize(null);
92 
93 			// Pass the scheduling rule to the synchronizer so that sync change
94 			// events and cache commits to disk are batched
95 			EclipseSynchronizer.getInstance().run(getUpdateRule(manager),
96 					monitor1 -> {
97 						try {
98 							runWithProjectRule(project, (SyncInfoSet) projectSyncInfos.get(project), monitor1);
99 						} catch (TeamException e) {
100 							throw CVSException.wrapException(e);
101 						}
102 					}, Policy.subMonitorFor(monitor, 100));
103 		} catch (TeamException e) {
104 			throw new InvocationTargetException(e);
105 		} catch (CoreException e) {
106 			throw new InvocationTargetException(e);
107 		}
108 	}
109 
getUpdateRule(SynchronizationScopeManager manager)110 	private ISchedulingRule getUpdateRule(SynchronizationScopeManager manager) {
111 		ISchedulingRule rule = null;
112 		ResourceMapping[] mappings = manager.getScope().getMappings();
113 		for (ResourceMapping mapping : mappings) {
114 			IProject[] mappingProjects = mapping.getProjects();
115 			for (IProject mappingProject : mappingProjects) {
116 				if (rule == null) {
117 					rule = mappingProject;
118 				} else {
119 					rule = MultiRule.combine(rule, mappingProject);
120 				}
121 			}
122 		}
123 		return rule;
124 	}
125 
126 	@Override
run(IProgressMonitor monitor)127 	public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
128 		skipped.clear();
129 		super.run(monitor);
130 		try {
131 			handleFailedUpdates(monitor);
132 		} catch (TeamException e) {
133 			throw new InvocationTargetException(e);
134 		}
135 	}
136 
137 	@Override
runWithProjectRule(IProject project, SyncInfoSet syncSet, IProgressMonitor monitor)138 	public void runWithProjectRule(IProject project, SyncInfoSet syncSet, IProgressMonitor monitor) throws TeamException {
139 		try {
140 			monitor.beginTask(null, 100);
141 
142 			// Remove the cases that are known to fail (adding them to skipped list)
143 			removeKnownFailureCases(syncSet);
144 
145 			// Run the update on the remaining nodes in the set
146 			// The update will fail for conflicts that turn out to be non-automergable
147 			safeUpdate(project, syncSet, Policy.subMonitorFor(monitor, 100));
148 
149 			// Remove all failed conflicts from the original sync set
150 			syncSet.rejectNodes(new FastSyncInfoFilter() {
151 				@Override
152 				public boolean select(SyncInfo info) {
153 					return skipped.getSyncInfo(info.getLocal()) != null;
154 				}
155 			});
156 
157 			// Signal for the ones that were updated
158 			updated(syncSet.getResources());
159 		} finally {
160 			monitor.done();
161 		}
162 	}
163 
164 	/**
165 	 * @param syncSet
166 	 * @return
167 	 */
removeKnownFailureCases(SyncInfoSet syncSet)168 	private SyncInfoSet removeKnownFailureCases(SyncInfoSet syncSet) {
169 		// First, remove any known failure cases
170 		FastSyncInfoFilter failFilter = getKnownFailureCases();
171 		SyncInfo[] willFail = syncSet.getNodes(failFilter);
172 		syncSet.rejectNodes(failFilter);
173 		for (SyncInfo info : willFail) {
174 			skipped.add(info);
175 		}
176 		return syncSet;
177 	}
178 
handleFailedUpdates(IProgressMonitor monitor)179 	private void handleFailedUpdates(IProgressMonitor monitor) throws TeamException {
180 		// Handle conflicting files that can't be merged, ask the user what should be done.
181 		if(! skipped.isEmpty()) {
182 			if(getOverwriteLocalChanges()) {
183 				// Ask the user if a replace should be performed on the remaining nodes
184 				if(promptForOverwrite(skipped)) {
185 					overwriteUpdate(skipped, monitor);
186 					if (!skipped.isEmpty()) {
187 						updated(skipped.getResources());
188 					}
189 				}
190 			} else {
191 				// Warn the user that some nodes could not be updated. This can happen if there are
192 				// files with conflicts that are not auto-mergeable.
193 				warnAboutFailedResources(skipped);
194 			}
195 		}
196 	}
197 
getOverwriteLocalChanges()198 	protected boolean getOverwriteLocalChanges(){
199 		return false;
200 	}
201 
202 	/**
203 	 * Perform a safe update on the resources in the provided set. Any included resources
204 	 * that cannot be updated safely wil be added to the skippedFiles list.
205 	 * @param syncSet the set containing the resources to be updated
206 	 * @param monitor
207 	 */
safeUpdate(IProject project, SyncInfoSet syncSet, IProgressMonitor monitor)208 	protected void safeUpdate(IProject project, SyncInfoSet syncSet, IProgressMonitor monitor) throws TeamException {
209 		SyncInfo[] changed = syncSet.getSyncInfos();
210 		if (changed.length == 0) return;
211 
212 		// The list of sync resources to be updated using "cvs update"
213 		List<SyncInfo> updateShallow = new ArrayList<>();
214 		// A list of sync resource folders which need to be created locally
215 		// (incoming addition or previously pruned)
216 		Set<SyncInfo> parentCreationElements = new HashSet<>();
217 		// A list of sync resources that are incoming deletions.
218 		// We do these first to avoid case conflicts
219 		List<SyncInfo> updateDeletions = new ArrayList<>();
220 
221 		for (SyncInfo changedNode : changed) {
222 			// Make sure that parent folders exist
223 			SyncInfo parent = getParent(changedNode);
224 			if (parent != null && isOutOfSync(parent)) {
225 				// We need to ensure that parents that are either incoming folder additions
226 				// or previously pruned folders are recreated.
227 				parentCreationElements.add(parent);
228 			}
229 
230 			IResource resource = changedNode.getLocal();
231 			int kind = changedNode.getKind();
232 			boolean willBeAttempted = false;
233 			if (resource.getType() == IResource.FILE) {
234 				// Not all change types will require a "cvs update"
235 				// Some can be deleted locally without performing an update
236 				switch (kind & SyncInfo.DIRECTION_MASK) {
237 					case SyncInfo.INCOMING:
238 						switch (kind & SyncInfo.CHANGE_MASK) {
239 							case SyncInfo.DELETION:
240 								// Incoming deletions can just be deleted instead of updated
241 								updateDeletions.add(changedNode);
242 								willBeAttempted = true;
243 								break;
244 							default:
245 								// add the file to the list of files to be updated
246 								updateShallow.add(changedNode);
247 								willBeAttempted = true;
248 								break;
249 						}
250 						break;
251 					case SyncInfo.CONFLICTING:
252 						switch (kind & SyncInfo.CHANGE_MASK) {
253 							case SyncInfo.CHANGE:
254 								// add the file to the list of files to be updated
255 								updateShallow.add(changedNode);
256 								willBeAttempted = true;
257 								break;
258 						}
259 						break;
260 				}
261 				if (!willBeAttempted) {
262 					skipped.add(syncSet.getSyncInfo(resource));
263 				}
264 			} else {
265 				// Special handling for folders to support shallow operations on files
266 				// (i.e. folder operations are performed using the sync info already
267 				// contained in the sync info.
268 				if (isOutOfSync(changedNode)) {
269 					parentCreationElements.add(changedNode);
270 				}
271 			}
272 
273 		}
274 		try {
275 			monitor.beginTask(null, 100);
276 
277 			if (updateDeletions.size() > 0) {
278 				runUpdateDeletions(updateDeletions.toArray(new SyncInfo[updateDeletions.size()]), Policy.subMonitorFor(monitor, 25));
279 			}
280 			if (parentCreationElements.size() > 0) {
281 				makeInSync(parentCreationElements.toArray(new SyncInfo[parentCreationElements.size()]), Policy.subMonitorFor(monitor, 25));
282 			}
283 			if (updateShallow.size() > 0) {
284 				runSafeUpdate(project, updateShallow.toArray(new SyncInfo[updateShallow.size()]), Policy.subMonitorFor(monitor, 50));
285 			}
286 		} finally {
287 			monitor.done();
288 		}
289 		return;
290 	}
291 
292 	/**
293 	 * Perform an overwrite (unsafe) update on the resources in the provided set.
294 	 * The passed sync set may containe resources from multiple projects and
295 	 * it cannot be assumed that any scheduling rule is held when this method
296 	 * is invoked.
297 	 * @param syncSet the set containing the resources to be updated
298 	 * @param monitor
299 	 */
overwriteUpdate(SyncInfoSet syncSet, IProgressMonitor monitor)300 	protected abstract void overwriteUpdate(SyncInfoSet syncSet, IProgressMonitor monitor) throws TeamException;
301 
302 	/*
303 	 * Return a filter which selects the cases that we know ahead of time
304 	 * will fail on an update
305 	 */
getKnownFailureCases()306 	protected FastSyncInfoFilter getKnownFailureCases() {
307 		return new OrSyncInfoFilter(new FastSyncInfoFilter[] {
308 			// Conflicting additions of files will fail
309 			new AndSyncInfoFilter(new FastSyncInfoFilter[] {
310 				FastSyncInfoFilter.getDirectionAndChangeFilter(SyncInfo.CONFLICTING, SyncInfo.ADDITION),
311 				new FastSyncInfoFilter() {
312 					@Override
313 					public boolean select(SyncInfo info) {
314 						return info.getLocal().getType() == IResource.FILE;
315 					}
316 				}
317 			}),
318 			// Conflicting changes of files will fail if the local is not managed
319 			// or is an addition
320 			new AndSyncInfoFilter(new FastSyncInfoFilter[] {
321 				FastSyncInfoFilter.getDirectionAndChangeFilter(SyncInfo.CONFLICTING, SyncInfo.CHANGE),
322 				new FastSyncInfoFilter() {
323 					@Override
324 					public boolean select(SyncInfo info) {
325 						if (info.getLocal().getType() == IResource.FILE) {
326 							try {
327 								ICVSFile cvsFile = CVSWorkspaceRoot.getCVSFileFor((IFile)info.getLocal());
328 								byte[] syncBytes = cvsFile.getSyncBytes();
329 								return (syncBytes == null || ResourceSyncInfo.isAddition(syncBytes));
330 							} catch (CVSException e) {
331 								CVSUIPlugin.log(e);
332 								// Fall though and try to update
333 							}
334 						}
335 						return false;
336 					}
337 				}
338 			}),
339 			// Conflicting changes involving a deletion on one side will aways fail
340 			new AndSyncInfoFilter(new FastSyncInfoFilter[] {
341 				FastSyncInfoFilter.getDirectionAndChangeFilter(SyncInfo.CONFLICTING, SyncInfo.CHANGE),
342 				new FastSyncInfoFilter() {
343 					@Override
344 					public boolean select(SyncInfo info) {
345 						IResourceVariant remote = info.getRemote();
346 						IResourceVariant base = info.getBase();
347 						if (info.getLocal().exists()) {
348 							// local != base and no remote will fail
349 							return (base != null && remote == null);
350 						} else {
351 							// no local and base != remote
352 							return (base != null && remote != null && !base.equals(remote));
353 						}
354 					}
355 				}
356 			}),
357 			// Conflicts where the file type is binary will work but are not merged
358 			// so they should be skipped
359 			new AndSyncInfoFilter(new FastSyncInfoFilter[] {
360 				FastSyncInfoFilter.getDirectionAndChangeFilter(SyncInfo.CONFLICTING, SyncInfo.CHANGE),
361 				new FastSyncInfoFilter() {
362 					@Override
363 					public boolean select(SyncInfo info) {
364 						IResource local = info.getLocal();
365 						if (local.getType() == IResource.FILE) {
366 							try {
367 								ICVSFile file = CVSWorkspaceRoot.getCVSFileFor((IFile)local);
368 								byte[] syncBytes = file.getSyncBytes();
369 								if (syncBytes != null) {
370 									return ResourceSyncInfo.isBinary(syncBytes);
371 								}
372 							} catch (CVSException e) {
373 								// There was an error obtaining or interpreting the sync bytes
374 								// Log it and skip the file
375 								CVSProviderPlugin.log(e);
376 								return true;
377 							}
378 						}
379 						return false;
380 					}
381 				}
382 			}),
383 			// Outgoing changes may not fail but they are skipped as well
384 			new SyncInfoDirectionFilter(SyncInfo.OUTGOING)
385 		});
386 	}
387 
388 	/**
389 	 * Warn user that some files could not be updated.
390 	 * Note: This method is designed to be overridden by test cases.
391 	 */
392 	protected void warnAboutFailedResources(final SyncInfoSet syncSet) {
393 		TeamUIPlugin.getStandardDisplay()
394 				.syncExec(() -> MessageDialog.openInformation(getShell(),
395 						CVSUIMessages.SafeUpdateAction_warnFilesWithConflictsTitle,
396 						CVSUIMessages.SafeUpdateAction_warnFilesWithConflictsDescription));
397 	}
398 
399 	/**
400 	 * This method is invoked for all resources in the sync set that are incoming deletions.
401 	 * It is done separately to allow deletions to be performed before additions that may
402 	 * be the same name with different letter case.
403 	 * @param nodes the SyncInfo nodes that are incoming deletions
404 	 * @param monitor
405 	 * @throws TeamException
406 	 */
407 	protected abstract void runUpdateDeletions(SyncInfo[] nodes, IProgressMonitor monitor) throws TeamException;
408 
409 	/**
410 	 * This method is invoked for all resources in the sync set that are incoming changes
411 	 * (but not deletions: @see runUpdateDeletions) or conflicting changes.
412 	 * This method should only update those conflicting resources that are automergable.
413 	 * @param project the project containing the nodes
414 	 * @param nodes the incoming or conflicting SyncInfo nodes
415 	 * @param monitor
416 	 * @throws TeamException
417 	 */
418 	protected abstract void runSafeUpdate(IProject project, SyncInfo[] nodes, IProgressMonitor monitor) throws TeamException;
419 
420 	protected void safeUpdate(IProject project, IResource[] resources, LocalOption[] localOptions, IProgressMonitor monitor) throws TeamException {
421 		try {
422 			UpdateOnlyMergableOperation operation = new UpdateOnlyMergableOperation(getPart(), project, resources, localOptions);
423 			operation.run(monitor);
424 			addSkippedFiles(operation.getSkippedFiles());
425 		} catch (InvocationTargetException e) {
426 			throw CVSException.wrapException(e);
427 		} catch (InterruptedException e) {
428 			Policy.cancelOperation();
429 		}
430 	}
431 
432 	/**
433 	 * Notification of all resource that were updated (either safely or othrwise)
434 	 */
435 	protected abstract void updated(IResource[] resources) throws TeamException;
436 
437 	private void addSkippedFiles(IFile[] files) {
438 		SyncInfoSet set = getSyncInfoSet();
439 		for (IFile file : files) {
440 			skipped.add(set.getSyncInfo(file));
441 		}
442 	}
443 
444 	@Override
445 	protected String getErrorTitle() {
446 		return CVSUIMessages.UpdateAction_update;
447 	}
448 
449 	@Override
450 	protected String getJobName() {
451 		SyncInfoSet syncSet = getSyncInfoSet();
452 		return NLS.bind(CVSUIMessages.UpdateAction_jobName, new String[] { Integer.valueOf(syncSet.size()).toString() });
453 	}
454 
455 	/**
456 	 * Confirm with the user what we are going to be doing. By default the update action doesn't
457 	 * prompt because the user has usually selected resources first. But in some cases, for example
458 	 * when performing a toolbar action, a confirmation prompt is nice.
459 	 * @param set the resources to be updated
460 	 * @return <code>true</code> if the update operation can continue, and <code>false</code>
461 	 * if the update has been cancelled by the user.
462 	 */
463 	private boolean promptIfNeeded() {
464 		final SyncInfoSet set = getSyncInfoSet();
465 		final boolean[] result = new boolean[] {true};
466 		if(getPromptBeforeUpdate()) {
467 			TeamUIPlugin.getStandardDisplay().syncExec(() -> {
468 				String sizeString = Integer.toString(set.size());
469 				String message = set.size() > 1
470 						? NLS.bind(CVSUIMessages.UpdateAction_promptForUpdateSeveral, new String[] { sizeString })
471 						: NLS.bind(CVSUIMessages.UpdateAction_promptForUpdateOne, new String[] { sizeString }); //
472 				result[0] = MessageDialog.openQuestion(getShell(),
473 						NLS.bind(CVSUIMessages.UpdateAction_promptForUpdateTitle, new String[] { sizeString }),
474 						message);
475 			});
476 		}
477 		return result[0];
478 	}
479 
480 	public boolean getPromptBeforeUpdate() {
481 		return promptBeforeUpdate;
482 	}
483 }
484