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