IDEA-212682 fixed: now projects marked as dirty cannot be reverted before import

Added FSM to control project status

GitOrigin-RevId: dd70cc820e71969f7c5dc8314fd9d16f160cd09b
This commit is contained in:
Sergei Vorobyov
2019-06-13 19:16:11 +03:00
committed by intellij-monorepo-bot
parent 19b7725ecf
commit 05629e906c
4 changed files with 306 additions and 58 deletions
@@ -11,6 +11,7 @@ import com.intellij.openapi.externalSystem.model.ProjectSystemId;
import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType;
import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode;
import com.intellij.openapi.externalSystem.service.internal.ExternalSystemProcessingManager;
import com.intellij.openapi.externalSystem.service.project.manage.ExternalProjectsManagerImpl;
import com.intellij.openapi.externalSystem.statistics.ExternalSystemActionsCollector;
import com.intellij.openapi.externalSystem.util.ExternalSystemBundle;
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil;
@@ -75,6 +76,7 @@ public class RefreshAllExternalProjectsAction extends AnAction implements AnActi
// We save all documents because there is a possible case that there is an external system config file changed inside the ide.
FileDocumentManager.getInstance().saveAllDocuments();
ExternalProjectsManagerImpl.disableProjectWatcherAutoUpdate(project);
for (ProjectSystemId externalSystemId : systemIds) {
ExternalSystemActionsCollector.trigger(project, externalSystemId, this, e);
ExternalSystemUtil.refreshProjects(
@@ -61,6 +61,7 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static com.intellij.util.ui.update.MergingUpdateQueue.ANY_COMPONENT;
@@ -92,8 +93,10 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
private final List<LocalFileSystem.WatchRequest> myWatchedRoots = new ArrayList<>();
private final MergingUpdateQueue myRefreshRequestsQueue;
private final Map<String, Long/*LocalTimeStamp*/> myStartUpdatesTimeStamps = Collections.synchronizedMap(new LinkedHashMap<>());
private final Map<String, Long/*LocalTimeStamp*/> myFileModificationTimeStamps = Collections.synchronizedMap(new LinkedHashMap<>());
private final Map<ExternalSystemTaskId, String/*ProjectPath*/> myProjectsInSync =
Collections.synchronizedMap(new LinkedHashMap<>());
private final Map<String/*ProjectPath*/, ProjectStatus> myWatchedProjects =
Collections.synchronizedMap(new LinkedHashMap<>());
private final CompoundParallelOperationTrace<ExternalSystemTaskId> syncTrace = new CompoundParallelOperationTrace<>();
@@ -138,7 +141,11 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
@Override
public void markDirtyAllExternalProjects() {
findLinkedProjectsSettings().forEach(this::scheduleUpdate);
findLinkedProjectsSettings().forEach(linkedProject -> {
String projectPath = getExternalProjectPath(linkedProject);
updateProjectStatus(projectPath, it -> it.markDirty(LocalTimeCounter.currentTime()));
scheduleUpdate(linkedProject);
});
for (Contributor contributor : EP_NAME.getExtensions()) {
contributor.markDirtyAllExternalProjects(myProject);
}
@@ -146,7 +153,9 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
@Override
public void markDirty(Module module) {
scheduleUpdate(ExternalSystemApiUtil.getExternalProjectPath(module));
String projectPath = ExternalSystemApiUtil.getExternalProjectPath(module);
updateProjectStatus(projectPath, it -> it.markDirty(LocalTimeCounter.currentTime()));
scheduleUpdate(projectPath);
for (Contributor contributor : EP_NAME.getExtensions()) {
contributor.markDirty(module);
}
@@ -154,6 +163,7 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
@Override
public void markDirty(String projectPath) {
updateProjectStatus(projectPath, it -> it.markDirty(LocalTimeCounter.currentTime()));
scheduleUpdate(projectPath);
for (Contributor contributor : EP_NAME.getExtensions()) {
contributor.markDirty(projectPath);
@@ -182,8 +192,6 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
String externalProjectPath = getRelatedExternalProjectPath(file);
if (externalProjectPath == null) return;
myFileModificationTimeStamps.put(file.getPath(), doc.getModificationStamp());
debug("Document changed '" + file.getPath() + "' from '" + event.getOldFragment() + "' to '" + event.getNewFragment() + "'");
synchronized (myChangedDocuments) {
myChangedDocuments.put(doc, Pair.create(externalProjectPath, file));
@@ -199,20 +207,29 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
}
ExternalSystemUtil.invokeLater(myProject, () -> WriteAction.run(
() -> copy.forEach((document, pair) -> {
if (!pair.second.isValid()) return;
if (fileWasChanged(pair.second)) {
scheduleUpdate(pair.first, false);
}
else {
handleRevertedChanges(externalProjectPath);
}
})
() -> copy.forEach((document, pair) -> handleDocumentChange(document, pair.first, pair.second))
));
}
});
}
private void handleDocumentChange(Document document, String externalProjectPath, VirtualFile file) {
if (!file.isValid()) return;
refreshFileCrcInfo(file);
if (fileWasChanged(file)) {
updateProjectStatus(externalProjectPath, it -> it.markModified(document.getModificationStamp()));
}
else if (!projectWasChanged(externalProjectPath)) {
updateProjectStatus(externalProjectPath, it -> it.markReverted(document.getModificationStamp()));
}
if (isUpToDate(externalProjectPath)) {
handleRevertedChanges(externalProjectPath);
}
else {
scheduleUpdate(externalProjectPath, false);
}
}
};
EditorFactory.getInstance().getEventMulticaster().addDocumentListener(myDocumentListener, myChangedDocumentsQueue);
ServiceManager.getService(ExternalSystemProgressNotificationManager.class).addNotificationListener(this);
@@ -253,7 +270,8 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
if (id.getType() == ExternalSystemTaskType.RESOLVE_PROJECT) {
debug("Refresh started for '" + workingDir + "'");
myStartUpdatesTimeStamps.put(workingDir, LocalTimeCounter.currentTime());
myProjectsInSync.put(id, workingDir);
updateProjectStatus(workingDir, it -> it.markSynchronized(LocalTimeCounter.currentTime()));
syncTrace.startTask(id);
@@ -283,14 +301,41 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
}
}
@Override
public void onFailure(@NotNull ExternalSystemTaskId id, @NotNull Exception e) {
if (id.getType() == ExternalSystemTaskType.RESOLVE_PROJECT) {
debug("Refresh failed");
syncTrace.finishTask(id);
String projectPath = myProjectsInSync.get(id);
updateProjectStatus(projectPath, it -> it.markDirty(LocalTimeCounter.currentTime()));
}
super.onFailure(id, e);
}
@Override
public void onCancel(@NotNull ExternalSystemTaskId id) {
if (id.getType() == ExternalSystemTaskType.RESOLVE_PROJECT) {
debug("Refresh cancel");
syncTrace.finishTask(id);
String projectPath = myProjectsInSync.get(id);
updateProjectStatus(projectPath, it -> it.markDirty(LocalTimeCounter.currentTime()));
}
super.onCancel(id);
}
@Override
public void onEnd(@NotNull ExternalSystemTaskId id) {
if (id.getType() == ExternalSystemTaskType.RESOLVE_PROJECT) {
syncTrace.finishTask(id);
myProjectsInSync.remove(id);
}
super.onEnd(id);
}
@Nullable
private static String getExternalProjectPath(@NotNull Pair<ExternalSystemManager, ExternalProjectSettings> linkedProject) {
return linkedProject.second.getExternalProjectPath();
}
private void scheduleUpdate(@Nullable String projectPath) {
scheduleUpdate(projectPath, true);
}
@@ -321,11 +366,9 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
debug("Update disabled for '" + projectPath + "'");
return;
}
if (manager instanceof ExternalSystemAutoImportAware) {
if (!updateIsNeededFor((ExternalSystemAutoImportAware)manager, projectPath)) {
debug("Update skipped for '" + projectPath + "'");
return;
}
if (isUpToDate(projectPath)) {
debug("Update skipped for '" + projectPath + "'");
return;
}
debug("Schedule update for '" + projectPath + "'");
@@ -372,17 +415,9 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
}
}
private void handleRevertedChanges(final String externalProjectPath) {
private void handleRevertedChanges(final @NotNull String externalProjectPath) {
debug("Handle reverted changes for '" + externalProjectPath + "'");
for (String filePath : new ArrayList<>(myKnownAffectedFiles.get(externalProjectPath))) {
VirtualFile f = VfsUtil.findFileByIoFile(new File(filePath), false);
if (f == null ||
!Objects.equals(f.getUserData(CRC_WITHOUT_SPACES_BEFORE_LAST_IMPORT), f.getUserData(CRC_WITHOUT_SPACES_CURRENT))) {
return;
}
}
ProjectSystemId systemId = getProjectSystemId(externalProjectPath);
if (systemId == null) return;
ExternalSystemManager<?, ?, ?, ?, ?> manager = ExternalSystemApiUtil.getManager(systemId);
@@ -402,36 +437,42 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
});
}
private static boolean fileWasChanged(VirtualFile file) {
if (!file.isValid()) {
return true;
}
Long newCrc = calculateCrc(file);
file.putUserData(CRC_WITHOUT_SPACES_CURRENT, newCrc);
Long crc = file.getUserData(CRC_WITHOUT_SPACES_BEFORE_LAST_IMPORT);
if (crc == null) {
file.putUserData(CRC_WITHOUT_SPACES_BEFORE_LAST_IMPORT, newCrc);
return true;
}
return !newCrc.equals(crc);
private static boolean fileWasChanged(@NotNull VirtualFile file) {
if (!file.isValid()) return true;
Long currentCrc = file.getUserData(CRC_WITHOUT_SPACES_CURRENT);
Long oldCrc = file.getUserData(CRC_WITHOUT_SPACES_BEFORE_LAST_IMPORT);
return currentCrc == null || !currentCrc.equals(oldCrc);
}
private boolean updateIsNeededFor(@NotNull ExternalSystemAutoImportAware manager, @NotNull String projectPath) {
Long lastUpdate = myStartUpdatesTimeStamps.get(projectPath);
if (lastUpdate == null) return true;
List<File> affectedFiles = manager.getAffectedExternalProjectFiles(projectPath, myProject);
for (File affectedFile : affectedFiles) {
String affectedFilePath = FileUtil.toCanonicalPath(affectedFile.getPath());
Long modificationStamp = myFileModificationTimeStamps.get(affectedFilePath);
if (modificationStamp != null && modificationStamp > lastUpdate) {
private boolean projectWasChanged(@NotNull String externalProjectPath) {
for (String filePath : new ArrayList<>(myKnownAffectedFiles.get(externalProjectPath))) {
VirtualFile file = VfsUtil.findFileByIoFile(new File(filePath), false);
if (file == null || fileWasChanged(file)) {
return true;
}
}
return false;
}
private static void refreshFileCrcInfo(@NotNull VirtualFile file) {
if (!file.isValid()) return;
Long crc = calculateCrc(file);
file.putUserData(CRC_WITHOUT_SPACES_CURRENT, crc);
}
private boolean isUpToDate(@NotNull String projectPath) {
return myWatchedProjects.getOrDefault(projectPath, new ProjectStatus()).isUpToDate();
}
private void updateProjectStatus(@Nullable String projectPath, @NotNull Consumer<ProjectStatus> update) {
if (projectPath == null) return;
myWatchedProjects.compute(projectPath, (path, status) -> {
status = status == null ? new ProjectStatus() : status;
update.accept(status);
return status;
});
}
private void addToRefreshQueue(String projectPath, ProjectSystemId systemId, boolean reportRefreshError) {
myRefreshRequestsQueue.queue(new Update(Pair.create(systemId, projectPath)) {
@Override
@@ -720,14 +761,20 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
@Override
protected void updateFile(VirtualFile file, VFileEvent event) {
myFileModificationTimeStamps.put(file.getPath(), file.getModificationStamp());
init();
init();
debug("File changed '" + file.getPath() + "'");
refreshFileCrcInfo(file);
if (event instanceof VFileContentChangeEvent && fileWasChanged(file)) {
for (String externalProjectPath : myKnownFiles.get(file.getPath())) {
updateProjectStatus(externalProjectPath, it -> it.markModified(file.getModificationStamp()));
}
filesToUpdate.add(file);
}
else {
for (String externalProjectPath : myKnownFiles.get(file.getPath())) {
if (projectWasChanged(externalProjectPath)) continue;
updateProjectStatus(externalProjectPath, it -> it.markReverted(file.getModificationStamp()));
if (!isUpToDate(externalProjectPath)) continue;
handleRevertedChanges(externalProjectPath);
}
}
@@ -735,12 +782,13 @@ public class ExternalSystemProjectsWatcherImpl extends ExternalSystemTaskNotific
@Override
protected void prepareFileDeletion(VirtualFile file) {
myFileModificationTimeStamps.put(file.getPath(), file.getModificationStamp());
for (String externalProjectPath : myKnownFiles.get(file.getPath())) {
updateProjectStatus(externalProjectPath, it -> it.markModified(file.getModificationStamp()));
}
init();
debug("File removed '" + file.getPath() + "'");
filesToRemove.add(file);
}
@Override
protected void apply() {
@@ -0,0 +1,98 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.externalSystem.service.project.autoimport
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.externalSystem.service.project.autoimport.ProjectStatus.ProjectEvent.*
import com.intellij.openapi.externalSystem.service.project.autoimport.ProjectStatus.ProjectState.*
import java.lang.Long.max
import java.util.concurrent.atomic.AtomicReference
class ProjectStatus {
private val LOG = Logger.getInstance(ProjectStatus::class.java)
private var state = AtomicReference(Synchronized(-1) as ProjectState)
fun isUpToDate() = when (state.get()) {
is Modified, is Dirty -> false
is Synchronized, is Reverted -> true
}
fun markDirty(stamp: Long): ProjectState {
return update(Invalidate(stamp))
}
fun markModified(stamp: Long): ProjectState {
return update(Modify(stamp))
}
fun markReverted(stamp: Long): ProjectState {
return update(Revert(stamp))
}
fun markSynchronized(stamp: Long): ProjectState {
return update(Synchronize(stamp))
}
fun update(event: ProjectEvent): ProjectState {
if (LOG.isDebugEnabled) {
LOG.debug("Event ${event::class.simpleName} is happened at ${event.stamp}")
}
return state.updateAndGet { currentState ->
when (currentState) {
is Synchronized -> when (event) {
is Synchronize -> event.withFuture(currentState, ::Synchronized)
is Invalidate -> event.ifFuture(currentState, ::Dirty)
is Modify -> event.ifFuture(currentState, ::Modified)
is Revert -> event.ifFuture(currentState, ::Reverted)
}
is Dirty -> when (event) {
is Synchronize -> event.ifFuture(currentState, ::Synchronized)
is Invalidate -> event.withFuture(currentState, ::Dirty)
is Modify -> event.withFuture(currentState, ::Dirty)
is Revert -> event.withFuture(currentState, ::Dirty)
}
is Modified -> when (event) {
is Synchronize -> event.ifFuture(currentState, ::Synchronized)
is Invalidate -> event.withFuture(currentState, ::Dirty)
is Modify -> event.withFuture(currentState, ::Modified)
is Revert -> event.ifFuture(currentState, ::Reverted)
}
is Reverted -> when (event) {
is Synchronize -> event.ifFuture(currentState, ::Synchronized)
is Invalidate -> event.withFuture(currentState, ::Dirty)
is Modify -> event.ifFuture(currentState, ::Modified)
is Revert -> event.withFuture(currentState, ::Reverted)
}
}
}
}
private fun ProjectEvent.withFuture(state: ProjectState, action: (Long) -> ProjectState): ProjectState {
return action(max(stamp, state.stamp))
}
private fun ProjectEvent.ifFuture(state: ProjectState, action: (Long) -> ProjectState): ProjectState {
return if (stamp > state.stamp) action(stamp) else state
}
sealed class ProjectEvent(val stamp: Long) {
class Synchronize(stamp: Long) : ProjectEvent(stamp)
class Invalidate(stamp: Long) : ProjectEvent(stamp)
class Modify(stamp: Long) : ProjectEvent(stamp)
class Revert(stamp: Long) : ProjectEvent(stamp)
}
sealed class ProjectState(val stamp: Long) {
class Synchronized(stamp: Long) : ProjectState(stamp)
class Dirty(stamp: Long) : ProjectState(stamp)
class Modified(stamp: Long) : ProjectState(stamp)
class Reverted(stamp: Long) : ProjectState(stamp)
}
}
@@ -0,0 +1,100 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.externalSystem.service.project.autoimport.states
import com.intellij.openapi.externalSystem.service.project.autoimport.ProjectStatus
import com.intellij.openapi.externalSystem.service.project.autoimport.ProjectStatus.ProjectState.*
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ProjectStatusTest {
@Test
fun `test open project with broken state`() {
val status = ProjectStatus()
status.markDirty(10) as Dirty
status.markModified(20) as Dirty
assertFalse(status.isUpToDate())
status.markSynchronized(30) as Synchronized
assertTrue(status.isUpToDate())
}
@Test
fun `test generate project with broken state`() {
val status = ProjectStatus()
status.markModified(10) as Modified
status.markDirty(20) as Dirty
assertFalse(status.isUpToDate())
status.markSynchronized(30) as Synchronized
assertTrue(status.isUpToDate())
}
@Test
fun `test generate project with lag and broken state`() {
val status = ProjectStatus()
status.markDirty(10) as Dirty
assertFalse(status.isUpToDate())
status.markSynchronized(30) as Synchronized
assertTrue(status.isUpToDate())
status.markModified(20) as Synchronized
assertTrue(status.isUpToDate())
}
@Test
fun `test delayed modification event`() {
val status = ProjectStatus()
status.markSynchronized(20) as Synchronized
status.markModified(10) as Synchronized
assertTrue(status.isUpToDate())
}
@Test
fun `test delayed invalidation event`() {
val status = ProjectStatus()
status.markSynchronized(20) as Synchronized
status.markDirty(10) as Synchronized
assertTrue(status.isUpToDate())
}
@Test
fun `test common sample`() {
val status = ProjectStatus()
status.markModified(10) as Modified
status.markModified(20) as Modified
status.markModified(30) as Modified
assertFalse(status.isUpToDate())
status.markSynchronized(40) as Synchronized
assertTrue(status.isUpToDate())
}
@Test
fun `test revert changes`() {
val status = ProjectStatus()
status.markModified(10) as Modified
status.markReverted(20) as Reverted
assertTrue(status.isUpToDate())
status.markSynchronized(30) as Synchronized
assertTrue(status.isUpToDate())
}
@Test
fun `test revert dirty changes`() {
val status = ProjectStatus()
status.markModified(10) as Modified
status.markDirty(20) as Dirty
status.markReverted(30) as Dirty
assertFalse(status.isUpToDate())
status.markSynchronized(40) as Synchronized
assertTrue(status.isUpToDate())
}
@Test
fun `test modification after revert event`() {
val status = ProjectStatus()
status.markModified(10) as Modified
status.markReverted(20) as Reverted
status.markModified(30) as Modified
assertFalse(status.isUpToDate())
status.markSynchronized(40) as Synchronized
assertTrue(status.isUpToDate())
}
}