in source/com.microsoft.tfs.client.eclipse/src/com/microsoft/tfs/client/eclipse/filemodification/TFSFileModificationValidator.java [127:463]
public IStatus validateEdit(final IFile[] files, final boolean attemptUi, final Object shell) {
final ResourceDataManager resourceDataManager = TFSEclipseClientPlugin.getDefault().getResourceDataManager();
final TFSRepository repository = repositoryProvider.getRepository();
if (repositoryProvider.getRepositoryStatus() == ProjectRepositoryStatus.CONNECTING) {
getStatusReporter(attemptUi, shell).reportError(
Messages.getString("TFSFileModificationValidator.ErrorConnectionInProgress"), //$NON-NLS-1$
new Status(
IStatus.ERROR,
TFSEclipseClientPlugin.PLUGIN_ID,
0,
Messages.getString("TFSFileModificationValidator.ErrorConnectionInProgressDescription"), //$NON-NLS-1$
null));
return Status.CANCEL_STATUS;
}
/*
* Offline server workspace. Simply mark files as writable and continue.
*/
if (repository == null) {
for (int i = 0; i < files.length; i++) {
log.info(MessageFormat.format("Setting {0} writable, project is offline from TFS server", files[i])); //$NON-NLS-1$
files[i].setReadOnly(false);
}
return Status.OK_STATUS;
}
/*
* Local workspace: ignore this entirely. This method is only called for
* read-only files. A read only file in a local workspace was not placed
* by us and we should not set them writable.
*/
if (WorkspaceLocation.LOCAL.equals(repository.getWorkspace().getLocation())) {
for (int i = 0; i < files.length; i++) {
log.info(MessageFormat.format("Ignoring read-only file {0} in local TFS workspace", files[i])); //$NON-NLS-1$
}
return Status.OK_STATUS;
}
/*
* HACK: avoid "phantom pending changes" that arise from the undo
* manager. If we have no undoable context (ie, no Shell), then check to
* see if we're being called from the undo manager and simply defer this
* operation.
*
* This bug is hard to reproduce, so details are thus far limited. The
* best guess is that the Eclipse undo manager watches edits using a
* resource changed listener (for post-change and post-build
* notifications.) It then realizes that the current contents of a file
* are identical to a previous undo state, and therefore needs to
* resynchronize the file history. It then, for some reason, calls
* validateEdit on that file. This causes the file to be checked out
* (here.)
*/
if (attemptUi == false && shell == null) {
/*
* Since this is a terrible hack, we may need to have users disable
* this functionality with a sysprop.
*/
if ("false".equalsIgnoreCase(System.getProperty(IGNORE_UNDO_MANAGER_PROPERTY_NAME)) == false) //$NON-NLS-1$
{
/* Build an exception to get our stack trace. */
final Exception e = new Exception(""); //$NON-NLS-1$
e.fillInStackTrace();
final StackTraceElement[] stackTrace = e.getStackTrace();
if (stackTrace != null) {
for (int i = 0; i < stackTrace.length; i++) {
if (stackTrace[i].getClassName().equals(
"org.eclipse.ui.internal.ide.undo.WorkspaceUndoMonitor")) //$NON-NLS-1$
{
log.info("Ignoring file modification request from WorkspaceUndoMonitor"); //$NON-NLS-1$
return Status.OK_STATUS;
}
}
}
}
}
/* Pend edits for these files. */
final List<String> pathList = new ArrayList<String>();
final Set<String> projectSet = new HashSet<String>();
for (int i = 0; i < files.length; i++) {
if (IGNORED_RESOURCES_FILTER.filter(files[i]) == ResourceFilterResult.REJECT) {
log.info(
MessageFormat.format("Setting {0} writable, file matches automatic validation filter", files[i])); //$NON-NLS-1$
files[i].setReadOnly(false);
continue;
}
/* Make sure that this file exists on the server. */
if (!resourceDataManager.hasResourceData(files[i])
&& resourceDataManager.hasCompletedRefresh(files[i].getProject())) {
continue;
}
final String path = Resources.getLocation(files[i], LocationUnavailablePolicy.IGNORE_RESOURCE);
final String serverPath = repository.getWorkspace().getMappedServerPath(path);
if (path == null) {
continue;
}
final PendingChange pendingChange = repository.getPendingChangeCache().getPendingChangeByLocalPath(path);
/* Don't pend changes when there's already an add or edit pended. */
if (pendingChange != null
&& (pendingChange.getChangeType().contains(ChangeType.ADD)
|| pendingChange.getChangeType().contains(ChangeType.EDIT))) {
log.debug(MessageFormat.format(
"File {0} has pending change {1}, ignoring", //$NON-NLS-1$
files[i],
pendingChange.getChangeType().toUIString(true, pendingChange)));
continue;
}
pathList.add(path);
log.info(MessageFormat.format("File {0} is being modified, checking out", files[i])); //$NON-NLS-1$
if (serverPath != null) {
projectSet.add(ServerPath.getTeamProject(serverPath));
}
}
if (pathList.size() == 0) {
return Status.OK_STATUS;
}
LockLevel forcedLockLevel = null;
boolean forcedGetLatest = false;
/*
* Query the server's default checkout lock and get latest on checkout
* setting
*/
for (final Iterator<String> i = projectSet.iterator(); i.hasNext();) {
final String teamProject = i.next();
final String exclusiveCheckoutAnnotation = repository.getAnnotationCache().getAnnotationValue(
VersionControlConstants.EXCLUSIVE_CHECKOUT_ANNOTATION,
teamProject,
0);
final String getLatestAnnotation = repository.getAnnotationCache().getAnnotationValue(
VersionControlConstants.GET_LATEST_ON_CHECKOUT_ANNOTATION,
teamProject,
0);
if ("true".equalsIgnoreCase(exclusiveCheckoutAnnotation)) //$NON-NLS-1$
{
forcedLockLevel = LockLevel.CHECKOUT;
break;
}
/* Server get latest on checkout forces us to work synchronously */
if ("true".equalsIgnoreCase(getLatestAnnotation)) //$NON-NLS-1$
{
forcedGetLatest = true;
}
}
/* Allow UI hooks to handle prompt before checkout. */
final TFSFileModificationOptions checkoutOptions =
getOptions(attemptUi, shell, pathList.toArray(new String[pathList.size()]), forcedLockLevel);
if (!checkoutOptions.getStatus().isOK()) {
return checkoutOptions.getStatus();
}
final String[] paths = checkoutOptions.getFiles();
final LockLevel lockLevel = checkoutOptions.getLockLevel();
final boolean getLatest = checkoutOptions.isGetLatest();
final boolean synchronousCheckout = checkoutOptions.isSynchronous();
final boolean foregroundCheckout = checkoutOptions.isForeground();
if (paths.length == 0) {
return Status.OK_STATUS;
}
final ItemSpec[] itemSpecs = new ItemSpec[paths.length];
for (int i = 0; i < paths.length; i++) {
itemSpecs[i] = new ItemSpec(paths[i], RecursionType.NONE);
}
/*
* Query get latest on checkout preference (and ensure server supports
* the feature)
*/
GetOptions getOptions = GetOptions.NO_DISK_UPDATE;
PendChangesOptions pendChangesOptions = PendChangesOptions.NONE;
if (repository.getWorkspace().getClient().getServerSupportedFeatures().contains(
SupportedFeatures.GET_LATEST_ON_CHECKOUT) && getLatest) {
/*
* If we're doing get latest on checkout, we need add the overwrite
* flag: we need to set the file writable before this method exits
* (in order for Eclipse to pick up the change, but we need to do
* the get in another thread (so that we can clear the resource lock
* on this file.) Thus we need to set the file writable, then fire a
* synchronous worker to overwrite it. This is safe as this method
* will ONLY be called when the file is readonly.
*/
pendChangesOptions = PendChangesOptions.GET_LATEST_ON_CHECKOUT;
getOptions = GetOptions.NONE;
}
/*
* Build the checkout command - no need to query conflicts here, the
* only conflicts that can arise from a pend edit are writable file
* conflicts (when get latest on checkout is true.) This method is never
* called for writable files.
*/
final EditCommand editCommand =
new EditCommand(repository, itemSpecs, lockLevel, null, getOptions, pendChangesOptions, false);
/*
* Pend changes in the foreground if get latest on checkout is
* requested. A disk update may be required, so we want to block user
* input.
*/
if (synchronousCheckout
|| pendChangesOptions.contains(PendChangesOptions.GET_LATEST_ON_CHECKOUT)
|| forcedGetLatest) {
/*
* Wrap this edit command in one that disables the plugin's
* automatic resource refresh behavior. This is required to avoid
* deadlocks: the calling thread has taken a resource lock on the
* resource it wishes to check out - the plugin will also require a
* resource lock to do the refresh in another thread.
*/
final ICommand wrappedEditCommand = new IgnoreResourceRefreshesEditCommand(editCommand);
final IStatus editStatus = getSynchronousCommandExecutor(attemptUi, shell).execute(wrappedEditCommand);
/* Refresh files on this thread, since it has the resource lock. */
for (int i = 0; i < files.length; i++) {
try {
files[i].refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor());
} catch (final Throwable e) {
log.warn(MessageFormat.format("Could not refresh {0}", files[i].getName()), e); //$NON-NLS-1$
}
}
return editStatus;
}
/* Pend changes in the background */
else {
synchronized (backgroundFiles) {
for (int i = 0; i < files.length; i++) {
files[i].setReadOnly(false);
backgroundFiles.put(files[i], new TFSFileModificationStatusData(files[i]));
}
}
final JobCommandAdapter editJob = new JobCommandAdapter(editCommand);
editJob.setPriority(Job.INTERACTIVE);
editJob.setUser(foregroundCheckout);
editJob.schedule();
final Thread editThread = new Thread(new Runnable() {
@Override
public void run() {
IStatus editStatus;
try {
/*
* We don't need to safe-wait with
* ExtensionPointAsyncObjectWaiter because we're
* guaranteed not on the UI thread.
*/
editJob.join();
editStatus = editJob.getResult();
} catch (final Exception e) {
editStatus = new Status(IStatus.ERROR, TFSEclipseClientPlugin.PLUGIN_ID, 0, null, e);
}
if (editStatus.isOK()) {
synchronized (backgroundFiles) {
for (int i = 0; i < files.length; i++) {
final TFSFileModificationStatusData statusData = backgroundFiles.remove(files[i]);
if (statusData != null) {
log.info(MessageFormat.format(
"File {0} checked out in {1} seconds", //$NON-NLS-1$
files[i],
(int) ((System.currentTimeMillis() - statusData.getStartTime()) / 1000)));
}
}
}
} else {
final List<TFSFileModificationStatusData> statusDataList =
new ArrayList<TFSFileModificationStatusData>();
synchronized (backgroundFiles) {
for (int i = 0; i < files.length; i++) {
final TFSFileModificationStatusData statusData = backgroundFiles.remove(files[i]);
if (statusData != null) {
log.info(MessageFormat.format(
"File {0} failed to check out in {1} seconds", //$NON-NLS-1$
files[i],
(int) ((System.currentTimeMillis() - statusData.getStartTime()) / 1000)));
statusDataList.add(statusData);
}
}
}
/*
* Unfortunately, we have to roll back ALL FILES when an
* edit fails. We could (in theory) be better about this
* and use the non fatal listener in EditCommand to give
* us the paths that failed, but at the moment, the use
* case is only for one file at a time, so this is okay.
*/
final TFSFileModificationStatusData[] statusData =
statusDataList.toArray(new TFSFileModificationStatusData[statusDataList.size()]);
getStatusReporter(attemptUi, shell).reportStatus(repository, statusData, editStatus);
}
}
});
editThread.start();
return Status.OK_STATUS;
}
}