diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 9b633d56ba..208ff51e5a 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/2142[#2142]: Move IDE-specific metadata (.idea, .vscode) out of workspace * https://github.com/devonfw/IDEasy/issues/989[#989]: Allow expressions in template variable definitions The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/50?closed=1[milestone 2026.09.002]. diff --git a/cli/src/main/java/com/devonfw/tools/ide/migration/IdeMigrator.java b/cli/src/main/java/com/devonfw/tools/ide/migration/IdeMigrator.java index a79d56afb4..23958c07ba 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/migration/IdeMigrator.java +++ b/cli/src/main/java/com/devonfw/tools/ide/migration/IdeMigrator.java @@ -9,6 +9,7 @@ import com.devonfw.tools.ide.log.IdeLogLevel; import com.devonfw.tools.ide.migration.v2025.Mig202502001; import com.devonfw.tools.ide.migration.v2025.Mig202510001; +import com.devonfw.tools.ide.migration.v2026.Mig202609002; import com.devonfw.tools.ide.step.Step; import com.devonfw.tools.ide.version.IdeVersion; import com.devonfw.tools.ide.version.VersionIdentifier; @@ -31,7 +32,7 @@ public class IdeMigrator implements IdeMigration { public IdeMigrator() { // migrations must be strictly in ascending order (from oldest to newest version) - this(List.of(new Mig202502001(), new Mig202510001())); + this(List.of(new Mig202502001(), new Mig202510001(), new Mig202609002())); } /** diff --git a/cli/src/main/java/com/devonfw/tools/ide/migration/v2026/Mig202609002.java b/cli/src/main/java/com/devonfw/tools/ide/migration/v2026/Mig202609002.java new file mode 100644 index 0000000000..d190ecdc76 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/migration/v2026/Mig202609002.java @@ -0,0 +1,55 @@ +package com.devonfw.tools.ide.migration.v2026; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.io.FileAccess; +import com.devonfw.tools.ide.migration.IdeVersionMigration; + +/** + * Migration to 2026.09.002. Moves the VSCode user-data folder ({@code .vscode/.userdata}) out of each workspace into the dedicated + * {@code $IDE_HOME/.ide/vscode/«workspace»/config} folder so workspaces stay clean and independent of the IDE being used. See + * #2142. + */ +public class Mig202609002 extends IdeVersionMigration { + + private static final Logger LOG = LoggerFactory.getLogger(Mig202609002.class); + + /** + * The constructor. + */ + public Mig202609002() { + + super("2026.09.002"); + } + + @Override + public void run(IdeContext context) { + + Path workspacesPath = context.getWorkspacesBasePath(); + if (workspacesPath == null) { + return; + } + FileAccess fileAccess = context.getFileAccess(); + Path vscodeMetaPath = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode"); + List workspaces = fileAccess.listChildren(workspacesPath, Files::isDirectory); + for (Path workspace : workspaces) { + Path oldUserData = workspace.resolve(".vscode").resolve(".userdata"); + if (fileAccess.isExpectedFolder(oldUserData)) { + Path target = vscodeMetaPath.resolve(workspace.getFileName().toString()).resolve("config"); + if (Files.exists(target)) { + LOG.warn("Skipping migration of {} since target already exists: {}", oldUserData, target); + continue; + } + fileAccess.mkdirs(target.getParent()); + fileAccess.move(oldUserData, target); + } + } + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/vscode/Vscode.java b/cli/src/main/java/com/devonfw/tools/ide/tool/vscode/Vscode.java index 57933bfd93..f1131384d1 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/tool/vscode/Vscode.java +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/vscode/Vscode.java @@ -86,7 +86,7 @@ protected void configureToolArgs(ProcessContext pc, ProcessMode processMode, Lis if (this.context.getSystemInfo().isWsl()) { pc.withEnvVar("DONT_PROMPT_WSL_INSTALL", "1"); } - Path vsCodeConf = this.context.getWorkspacePath().resolve(".vscode/.userdata"); + Path vsCodeConf = getIdeMetadataPath().resolve("config"); pc.addArg("--new-window"); pc.addArg("--user-data-dir=" + vsCodeConf); Path vsCodeExtensionFolder = this.context.getIdeHome().resolve("plugins/vscode"); diff --git a/cli/src/test/java/com/devonfw/tools/ide/migration/v2026/Mig202609002Test.java b/cli/src/test/java/com/devonfw/tools/ide/migration/v2026/Mig202609002Test.java new file mode 100644 index 0000000000..b5c833535d --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/migration/v2026/Mig202609002Test.java @@ -0,0 +1,74 @@ +package com.devonfw.tools.ide.migration.v2026; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.io.FileAccess; + +/** + * Test of {@link Mig202609002}. + */ +class Mig202609002Test extends AbstractIdeContextTest { + + /** + * Tests that an existing {@code .vscode/.userdata} folder is moved out of the workspace into {@code $IDE_HOME/.ide/vscode/«workspace»/config}. + */ + @Test + void testMovesVscodeUserDataOutOfWorkspace() { + + // arrange + IdeTestContext context = newContext("vscode"); + FileAccess fileAccess = context.getFileAccess(); + Path workspace = context.getWorkspacePath(); + Path oldUserData = workspace.resolve(".vscode").resolve(".userdata"); + fileAccess.mkdirs(oldUserData); + fileAccess.writeFileContent("dummy", oldUserData.resolve("state.json")); + // act + new Mig202609002().run(context); + // assert + Path newConfig = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode").resolve(context.getWorkspaceName()).resolve("config"); + assertThat(newConfig.resolve("state.json")).exists().hasContent("dummy"); + assertThat(oldUserData).doesNotExist(); + } + + /** + * Tests that the migration is a no-op (and does not fail) when no {@code .vscode/.userdata} folder exists. + */ + @Test + void testDoesNothingWhenNoUserData() { + + // arrange + IdeTestContext context = newContext("vscode"); + Path vscodeMeta = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode"); + // act + new Mig202609002().run(context); + // assert + assertThat(vscodeMeta).doesNotExist(); + } + + /** + * Tests that the migration skips a workspace whose target folder already exists, without failing and without overwriting the existing data. + */ + @Test + void testSkipsWhenTargetAlreadyExists() { + + // arrange + IdeTestContext context = newContext("vscode"); + FileAccess fileAccess = context.getFileAccess(); + Path oldUserData = context.getWorkspacePath().resolve(".vscode").resolve(".userdata"); + fileAccess.mkdirs(oldUserData); + fileAccess.writeFileContent("old", oldUserData.resolve("state.json")); + Path target = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode").resolve(context.getWorkspaceName()).resolve("config"); + fileAccess.mkdirs(target); + fileAccess.writeFileContent("new", target.resolve("state.json")); + // act + new Mig202609002().run(context); + // assert + assertThat(oldUserData).exists(); + assertThat(target.resolve("state.json")).exists().hasContent("new"); + } +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/vscode/VscodeTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/vscode/VscodeTest.java index 72e57032a0..c6ecaada29 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/tool/vscode/VscodeTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/vscode/VscodeTest.java @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.tool.vscode; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -10,6 +11,7 @@ import org.junit.jupiter.api.Test; import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeContext; import com.devonfw.tools.ide.context.IdeTestContext; import com.devonfw.tools.ide.context.ProcessContextTestImpl; import com.devonfw.tools.ide.environment.EnvironmentVariablesType; @@ -152,6 +154,26 @@ void testConfigureToolArgsDoesNotSetWslEnvVarOnNonWsl() { assertThat(pc.getEnvVar("DONT_PROMPT_WSL_INSTALL")).isNull(); } + /** + * Tests that {@link Vscode#configureToolArgs(ProcessContext, ProcessMode, List)} points {@code --user-data-dir} to the IDE metadata folder + * ({@code $IDE_HOME/.ide/vscode/«workspace»/config}) instead of a {@code .vscode} folder inside the workspace. + */ + @Test + void testConfigureToolArgsUsesIdeMetadataPathForUserData() { + + // arrange + IdeTestContext context = newContext(PROJECT_VSCODE); + context.setSystemInfo(SystemInfoMock.LINUX_X64); + Vscode commandlet = new Vscode(context); + ArgCapturingProcessContext pc = new ArgCapturingProcessContext(context); + // act + commandlet.configureToolArgs(pc, ProcessMode.DEFAULT, List.of()); + // assert + Path expectedUserData = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode").resolve(context.getWorkspaceName()).resolve("config"); + assertThat(pc.capturedArgs).contains("--user-data-dir=" + expectedUserData); + assertThat(pc.capturedArgs).noneMatch(arg -> arg.contains(".vscode")); + } + /** * Tests that {@code VSCODE_OPTIONS} is honoured by appending its tokens as additional command-line arguments when starting the IDE (analogue to the * global {@code IDE_OPTIONS} used for IDEasy itself, see issue #788). @@ -300,6 +322,26 @@ String getEnvVar(String key) { } } + /** + * {@link ProcessContextTestImpl} subclass that captures the CLI arguments added via {@link #addArg(String)} for test assertions. + */ + private static class ArgCapturingProcessContext extends ProcessContextTestImpl { + + private final List capturedArgs = new ArrayList<>(); + + private ArgCapturingProcessContext(IdeTestContext context) { + + super(context); + } + + @Override + public ProcessContext addArg(String arg) { + + this.capturedArgs.add(arg); + return super.addArg(arg); + } + } + private void checkVscodiumInstallation(IdeTestContext context) { assertThat(context.getSoftwarePath().resolve("vscode/bin/codium.cmd")).exists().hasContent("@echo test for windows");