Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: f530db49675c02bb0d0c793936ccd628e664a79c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
/*******************************************************************************
 * Copyright (c) 2000, 2010 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
 *******************************************************************************/
package org.eclipse.team.internal.ccvs.ui.subscriber;

import java.lang.reflect.InvocationTargetException;
import java.util.*;

import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.core.resources.*;
import org.eclipse.core.resources.mapping.ResourceMapping;
import org.eclipse.core.resources.mapping.ResourceMappingContext;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.MultiRule;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.osgi.util.NLS;
import org.eclipse.team.core.TeamException;
import org.eclipse.team.core.mapping.provider.SynchronizationScopeManager;
import org.eclipse.team.core.synchronize.*;
import org.eclipse.team.core.synchronize.FastSyncInfoFilter.*;
import org.eclipse.team.core.variants.IResourceVariant;
import org.eclipse.team.internal.ccvs.core.*;
import org.eclipse.team.internal.ccvs.core.client.Command.LocalOption;
import org.eclipse.team.internal.ccvs.core.resources.CVSWorkspaceRoot;
import org.eclipse.team.internal.ccvs.core.resources.EclipseSynchronizer;
import org.eclipse.team.internal.ccvs.core.syncinfo.ResourceSyncInfo;
import org.eclipse.team.internal.ccvs.ui.CVSUIMessages;
import org.eclipse.team.internal.ccvs.ui.CVSUIPlugin;
import org.eclipse.team.internal.ccvs.ui.Policy;
import org.eclipse.team.internal.ccvs.ui.operations.*;
import org.eclipse.team.internal.ui.TeamUIPlugin;
import org.eclipse.team.internal.ui.Utils;
import org.eclipse.team.ui.synchronize.ISynchronizePageConfiguration;

/**
 * This update action will update all mergable resources first and then prompt the
 * user to overwrite any resources that failed the safe update.
 * 
 * Subclasses should determine how the update should handle conflicts by implementing 
 * the getOverwriteLocalChanges() method.
 */
public abstract class SafeUpdateOperation extends CVSSubscriberOperation {

	private boolean promptBeforeUpdate = false;
	
	private SyncInfoSet skipped = new SyncInfoSet();
	
	protected SafeUpdateOperation(ISynchronizePageConfiguration configuration, IDiffElement[] elements, boolean promptBeforeUpdate) {
		super(configuration, elements);
		this.promptBeforeUpdate = promptBeforeUpdate;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.team.ui.TeamOperation#shouldRun()
	 */
	@Override
	public boolean shouldRun() {
		return promptIfNeeded();
	}

	/**
	 * Run the operation for the sync infos from the given project.
	 * 
	 * @param projectSyncInfos the project syncInfos
	 * @param project the project
	 * @param monitor a progress monitor
	 * @throws InvocationTargetException
	 */
	@Override
	protected void run(final Map projectSyncInfos, final IProject project,
			IProgressMonitor monitor) throws InvocationTargetException {
		try {
			IResource[] resources = getIResourcesFrom(((SyncInfoSet) projectSyncInfos
					.get(project)).getSyncInfos());
			ResourceMapping[] selectedMappings = Utils
					.getResourceMappings(resources);
			ResourceMappingContext context = new SingleProjectSubscriberContext(
					CVSProviderPlugin.getPlugin().getCVSWorkspaceSubscriber(),
					false, project);
			SynchronizationScopeManager manager = new SingleProjectScopeManager(
					getJobName(), selectedMappings, context, true, project);
			manager.initialize(null);

			// Pass the scheduling rule to the synchronizer so that sync change
			// events and cache commits to disk are batched
			EclipseSynchronizer.getInstance().run(getUpdateRule(manager),
					monitor1 -> {
						try {
							runWithProjectRule(project, (SyncInfoSet) projectSyncInfos.get(project), monitor1);
						} catch (TeamException e) {
							throw CVSException.wrapException(e);
						}
					}, Policy.subMonitorFor(monitor, 100));
		} catch (TeamException e) {
			throw new InvocationTargetException(e);
		} catch (CoreException e) {
			throw new InvocationTargetException(e);
		}
	}

	private ISchedulingRule getUpdateRule(SynchronizationScopeManager manager) {
		ISchedulingRule rule = null;
		ResourceMapping[] mappings = manager.getScope().getMappings();
		for (int i = 0; i < mappings.length; i++) {
			ResourceMapping mapping = mappings[i];
			IProject[] mappingProjects = mapping.getProjects();
			for (int j = 0; j < mappingProjects.length; j++) {
				if (rule == null) {
					rule = mappingProjects[j];
				} else {
					rule = MultiRule.combine(rule, mappingProjects[j]);
				}
			}
		}
		return rule;
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.team.internal.ccvs.ui.subscriber.CVSSubscriberOperation#run(org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
		skipped.clear();
		super.run(monitor);
		try {
			handleFailedUpdates(monitor);
		} catch (TeamException e) {
			throw new InvocationTargetException(e);
		}
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.team.internal.ccvs.ui.subscriber.CVSSubscriberAction#run(org.eclipse.team.ui.sync.SyncInfoSet, org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public void runWithProjectRule(IProject project, SyncInfoSet syncSet, IProgressMonitor monitor) throws TeamException {
		try {
			monitor.beginTask(null, 100);
			
			// Remove the cases that are known to fail (adding them to skipped list)
			removeKnownFailureCases(syncSet);
			
			// Run the update on the remaining nodes in the set
			// The update will fail for conflicts that turn out to be non-automergable
			safeUpdate(project, syncSet, Policy.subMonitorFor(monitor, 100));
			
			// Remove all failed conflicts from the original sync set
			syncSet.rejectNodes(new FastSyncInfoFilter() {
				@Override
				public boolean select(SyncInfo info) {
					return skipped.getSyncInfo(info.getLocal()) != null;
				}
			});
			
			// Signal for the ones that were updated
			updated(syncSet.getResources());
		} finally {
			monitor.done();
		}
	}

	/**
	 * @param syncSet
	 * @return
	 */
	private SyncInfoSet removeKnownFailureCases(SyncInfoSet syncSet) {
		// First, remove any known failure cases
		FastSyncInfoFilter failFilter = getKnownFailureCases();
		SyncInfo[] willFail = syncSet.getNodes(failFilter);
		syncSet.rejectNodes(failFilter);
		for (int i = 0; i < willFail.length; i++) {
			SyncInfo info = willFail[i];
			skipped.add(info);
		}
		return syncSet;
	}

	private void handleFailedUpdates(IProgressMonitor monitor) throws TeamException {
		// Handle conflicting files that can't be merged, ask the user what should be done.
		if(! skipped.isEmpty()) {
			if(getOverwriteLocalChanges()) {				
				// Ask the user if a replace should be performed on the remaining nodes
				if(promptForOverwrite(skipped)) {
					overwriteUpdate(skipped, monitor);
					if (!skipped.isEmpty()) {
						updated(skipped.getResources());
					}
				}
			} else {
				// Warn the user that some nodes could not be updated. This can happen if there are
				// files with conflicts that are not auto-mergeable.					
				warnAboutFailedResources(skipped);		
			}
		}
	}
	
	protected boolean getOverwriteLocalChanges(){
		return false;
	}

	/**
	 * Perform a safe update on the resources in the provided set. Any included resources
	 * that cannot be updated safely wil be added to the skippedFiles list.
	 * @param syncSet the set containing the resources to be updated
	 * @param monitor
	 */
	protected void safeUpdate(IProject project, SyncInfoSet syncSet, IProgressMonitor monitor) throws TeamException {
		SyncInfo[] changed = syncSet.getSyncInfos();
		if (changed.length == 0) return;
		
		// The list of sync resources to be updated using "cvs update"
		List<SyncInfo> updateShallow = new ArrayList<>();
		// A list of sync resource folders which need to be created locally 
		// (incoming addition or previously pruned)
		Set<SyncInfo> parentCreationElements = new HashSet<>();
		// A list of sync resources that are incoming deletions.
		// We do these first to avoid case conflicts
		List<SyncInfo> updateDeletions = new ArrayList<>();
	
		for (int i = 0; i < changed.length; i++) {
			SyncInfo changedNode = changed[i];
			
			// Make sure that parent folders exist
			SyncInfo parent = getParent(changedNode);
			if (parent != null && isOutOfSync(parent)) {
				// We need to ensure that parents that are either incoming folder additions
				// or previously pruned folders are recreated.
				parentCreationElements.add(parent);
			}
			
			IResource resource = changedNode.getLocal();
			int kind = changedNode.getKind();
			boolean willBeAttempted = false;
			if (resource.getType() == IResource.FILE) {	
				// Not all change types will require a "cvs update"
				// Some can be deleted locally without performing an update
				switch (kind & SyncInfo.DIRECTION_MASK) {
					case SyncInfo.INCOMING:
						switch (kind & SyncInfo.CHANGE_MASK) {
							case SyncInfo.DELETION:
								// Incoming deletions can just be deleted instead of updated
								updateDeletions.add(changedNode);
								willBeAttempted = true;
								break;
							default:
								// add the file to the list of files to be updated
								updateShallow.add(changedNode);
								willBeAttempted = true;
								break;
						}
						break;
					case SyncInfo.CONFLICTING:
						switch (kind & SyncInfo.CHANGE_MASK) {
							case SyncInfo.CHANGE:
								// add the file to the list of files to be updated
								updateShallow.add(changedNode);
								willBeAttempted = true;
								break;
						}
						break;
				}
				if (!willBeAttempted) {
					skipped.add(syncSet.getSyncInfo(resource));
				}
			} else {
				// Special handling for folders to support shallow operations on files
				// (i.e. folder operations are performed using the sync info already
				// contained in the sync info.
				if (isOutOfSync(changedNode)) {
					parentCreationElements.add(changedNode);
				}
			}

		}
		try {
			monitor.beginTask(null, 100);

			if (updateDeletions.size() > 0) {
				runUpdateDeletions(updateDeletions.toArray(new SyncInfo[updateDeletions.size()]), Policy.subMonitorFor(monitor, 25));
			}			
			if (parentCreationElements.size() > 0) {
				makeInSync(parentCreationElements.toArray(new SyncInfo[parentCreationElements.size()]), Policy.subMonitorFor(monitor, 25));				
			}
			if (updateShallow.size() > 0) {
				runSafeUpdate(project, updateShallow.toArray(new SyncInfo[updateShallow.size()]), Policy.subMonitorFor(monitor, 50));
			}
		} finally {
			monitor.done();
		}
		return;
	}

	/**
	 * Perform an overwrite (unsafe) update on the resources in the provided set.
	 * The passed sync set may containe resources from multiple projects and
	 * it cannot be assumed that any scheduling rule is held when this method
	 * is invoked.
	 * @param syncSet the set containing the resources to be updated
	 * @param monitor
	 */
	protected abstract void overwriteUpdate(SyncInfoSet syncSet, IProgressMonitor monitor) throws TeamException;

	/*
	 * Return a filter which selects the cases that we know ahead of time
	 * will fail on an update
	 */
	protected FastSyncInfoFilter getKnownFailureCases() {
		return new OrSyncInfoFilter(new FastSyncInfoFilter[] {
			// Conflicting additions of files will fail
			new AndSyncInfoFilter(new FastSyncInfoFilter[] {
				FastSyncInfoFilter.getDirectionAndChangeFilter(SyncInfo.CONFLICTING, SyncInfo.ADDITION),
				new FastSyncInfoFilter() {
					@Override
					public boolean select(SyncInfo info) {
						return info.getLocal().getType() == IResource.FILE;
					}
				}
			}),
			// Conflicting changes of files will fail if the local is not managed
			// or is an addition
			new AndSyncInfoFilter(new FastSyncInfoFilter[] {
				FastSyncInfoFilter.getDirectionAndChangeFilter(SyncInfo.CONFLICTING, SyncInfo.CHANGE),
				new FastSyncInfoFilter() {
					@Override
					public boolean select(SyncInfo info) {
						if (info.getLocal().getType() == IResource.FILE) {
							try {
								ICVSFile cvsFile = CVSWorkspaceRoot.getCVSFileFor((IFile)info.getLocal());
								byte[] syncBytes = cvsFile.getSyncBytes();
								return (syncBytes == null || ResourceSyncInfo.isAddition(syncBytes));
							} catch (CVSException e) {
								CVSUIPlugin.log(e);
								// Fall though and try to update
							}
						}
						return false;
					}
				}
			}),
			// Conflicting changes involving a deletion on one side will aways fail
			new AndSyncInfoFilter(new FastSyncInfoFilter[] {
				FastSyncInfoFilter.getDirectionAndChangeFilter(SyncInfo.CONFLICTING, SyncInfo.CHANGE),
				new FastSyncInfoFilter() {
					@Override
					public boolean select(SyncInfo info) {
						IResourceVariant remote = info.getRemote();
						IResourceVariant base = info.getBase();
						if (info.getLocal().exists()) {
							// local != base and no remote will fail
							return (base != null && remote == null);
						} else {
							// no local and base != remote
							return (base != null && remote != null && !base.equals(remote));
						}
					}
				}
			}),
			// Conflicts where the file type is binary will work but are not merged
			// so they should be skipped
			new AndSyncInfoFilter(new FastSyncInfoFilter[] {
				FastSyncInfoFilter.getDirectionAndChangeFilter(SyncInfo.CONFLICTING, SyncInfo.CHANGE),
				new FastSyncInfoFilter() {
					@Override
					public boolean select(SyncInfo info) {
						IResource local = info.getLocal();
						if (local.getType() == IResource.FILE) {
							try {
								ICVSFile file = CVSWorkspaceRoot.getCVSFileFor((IFile)local);
								byte[] syncBytes = file.getSyncBytes();
								if (syncBytes != null) {
									return ResourceSyncInfo.isBinary(syncBytes);
								}
							} catch (CVSException e) {
								// There was an error obtaining or interpreting the sync bytes
								// Log it and skip the file
								CVSProviderPlugin.log(e);
								return true;
							}
						}
						return false;
					}
				}
			}),
			// Outgoing changes may not fail but they are skipped as well
			new SyncInfoDirectionFilter(SyncInfo.OUTGOING)
		});
	}
	
	/**
	 * Warn user that some files could not be updated.
	 * Note: This method is designed to be overridden by test cases.
	 */
	protected void warnAboutFailedResources(final SyncInfoSet syncSet) {
		TeamUIPlugin.getStandardDisplay()
				.syncExec(() -> MessageDialog.openInformation(getShell(),
						CVSUIMessages.SafeUpdateAction_warnFilesWithConflictsTitle,
						CVSUIMessages.SafeUpdateAction_warnFilesWithConflictsDescription));
	}
	
	/**
	 * This method is invoked for all resources in the sync set that are incoming deletions.
	 * It is done separately to allow deletions to be performed before additions that may
	 * be the same name with different letter case.
	 * @param nodes the SyncInfo nodes that are incoming deletions
	 * @param monitor
	 * @throws TeamException
	 */
	protected abstract void runUpdateDeletions(SyncInfo[] nodes, IProgressMonitor monitor) throws TeamException;
	
	/**
	 * This method is invoked for all resources in the sync set that are incoming changes
	 * (but not deletions: @see runUpdateDeletions) or conflicting changes.
	 * This method should only update those conflicting resources that are automergable.
	 * @param project the project containing the nodes
	 * @param nodes the incoming or conflicting SyncInfo nodes
	 * @param monitor
	 * @throws TeamException
	 */
	protected abstract void runSafeUpdate(IProject project, SyncInfo[] nodes, IProgressMonitor monitor) throws TeamException;
	
	protected void safeUpdate(IProject project, IResource[] resources, LocalOption[] localOptions, IProgressMonitor monitor) throws TeamException {
		try {
			UpdateOnlyMergableOperation operation = new UpdateOnlyMergableOperation(getPart(), project, resources, localOptions);
			operation.run(monitor);
			addSkippedFiles(operation.getSkippedFiles());
		} catch (InvocationTargetException e) {
			throw CVSException.wrapException(e);
		} catch (InterruptedException e) {
			Policy.cancelOperation();
		}
	}
	
	/**
	 * Notification of all resource that were updated (either safely or othrwise)
	 */
	protected abstract void updated(IResource[] resources) throws TeamException;
	
	private void addSkippedFiles(IFile[] files) {
		SyncInfoSet set = getSyncInfoSet();
		for (int i = 0; i < files.length; i++) {
			IFile file = files[i];
			skipped.add(set.getSyncInfo(file));
		}
	}
	
	@Override
	protected String getErrorTitle() {
		return CVSUIMessages.UpdateAction_update; 
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.team.internal.ccvs.ui.subscriber.CVSSubscriberAction#getJobName(org.eclipse.team.ui.sync.SyncInfoSet)
	 */
	@Override
	protected String getJobName() {
		SyncInfoSet syncSet = getSyncInfoSet();
		return NLS.bind(CVSUIMessages.UpdateAction_jobName, new String[] { Integer.valueOf(syncSet.size()).toString() }); 
	}

	/**
	 * Confirm with the user what we are going to be doing. By default the update action doesn't 
	 * prompt because the user has usually selected resources first. But in some cases, for example
	 * when performing a toolbar action, a confirmation prompt is nice.
	 * @param set the resources to be updated
	 * @return <code>true</code> if the update operation can continue, and <code>false</code>
	 * if the update has been cancelled by the user.
	 */
	private boolean promptIfNeeded() {
		final SyncInfoSet set = getSyncInfoSet();
		final boolean[] result = new boolean[] {true};
		if(getPromptBeforeUpdate()) {
			TeamUIPlugin.getStandardDisplay().syncExec(() -> {
				String sizeString = Integer.toString(set.size());
				String message = set.size() > 1
						? NLS.bind(CVSUIMessages.UpdateAction_promptForUpdateSeveral, new String[] { sizeString })
						: NLS.bind(CVSUIMessages.UpdateAction_promptForUpdateOne, new String[] { sizeString }); //
				result[0] = MessageDialog.openQuestion(getShell(),
						NLS.bind(CVSUIMessages.UpdateAction_promptForUpdateTitle, new String[] { sizeString }),
						message);
			});
		}
		return result[0];
	}
	
	public boolean getPromptBeforeUpdate() {
		return promptBeforeUpdate;
	}
}

Back to the top