Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Release with new features and bugfixes:
* https://github.com/devonfw/IDEasy/issues/1853[#1853]: Add ARM releases for VSCode on Mac
* https://github.com/devonfw/IDEasy/issues/797[#797]: Use system unzip on macOS to preserve symlinks in ZIP extraction
* https://github.com/devonfw/IDEasy/issues/1723[#1723]: Add commandlet for GitHub Copilot CLI
* https://github.com/devonfw/IDEasy/issues/1695[#1695]: Clone settings to temporary directory, analyse, and then move
* https://github.com/devonfw/IDEasy/issues/1880[#1880]: Reinstall all plugins for IDE in force mode
* https://github.com/devonfw/IDEasy/issues/861[#861]: Fix install of pgadmin throws IllegalStateException when the install wizard starts
* https://github.com/devonfw/IDEasy/issues/1844[#1844]: VSCode plugin installation progress freezing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.devonfw.tools.ide.git.GitUrl;
import com.devonfw.tools.ide.git.repository.RepositoryCommandlet;
import com.devonfw.tools.ide.io.FileAccess;
import com.devonfw.tools.ide.io.FileCopyMode;
import com.devonfw.tools.ide.property.FlagProperty;
import com.devonfw.tools.ide.property.StringProperty;
import com.devonfw.tools.ide.step.Step;
Expand All @@ -35,6 +36,7 @@
import com.devonfw.tools.ide.tool.extra.ExtraToolsMapper;
import com.devonfw.tools.ide.variable.IdeVariables;
import com.devonfw.tools.ide.version.VersionIdentifier;
import com.devonfw.tools.ide.environment.EnvironmentVariables;

/**
* Abstract {@link Commandlet} base-class for both {@link UpdateCommandlet} and {@link CreateCommandlet}.
Expand Down Expand Up @@ -108,6 +110,8 @@ protected void doRun() {
createStartScripts();
}



private void reloadContext() {

((AbstractIdeContext) this.context).reload();
Expand Down Expand Up @@ -197,7 +201,6 @@ private void updateSettingsInStep() {
}

GitUrl gitUrl = getOrAskSettingsUrl();
checkProjectNameConvention(gitUrl.getProjectName());
initializeRepository(gitUrl);
}
}
Expand All @@ -206,17 +209,10 @@ private GitUrl getOrAskSettingsUrl() {

String repository = this.settingsRepo.getValue();
repository = handleDefaultRepository(repository);
String userPromt;
String defaultUrl;
if (isCodeRepository()) {
userPromt = "Code repository URL:";
defaultUrl = null;
LOG.info(MESSAGE_CODE_REPO_URL);
} else {
userPromt = "Settings URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:";
defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL;
LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath());
}
String userPromt = "Repository URL [" + IdeContext.DEFAULT_SETTINGS_REPO_URL + "]:";
String defaultUrl = IdeContext.DEFAULT_SETTINGS_REPO_URL;
LOG.info(MESSAGE_SETTINGS_REPO_URL, this.context.getSettingsPath());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fine but you then also need to include an update to the documentation that otherwise gets inconsistent:
https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc#code-repository


GitUrl gitUrl = null;
if (repository != null) {
gitUrl = GitUrl.of(repository);
Expand All @@ -234,61 +230,21 @@ private GitUrl getOrAskSettingsUrl() {

private String handleDefaultRepository(String repository) {
if ("-".equals(repository)) {
if (isCodeRepository()) {
LOG.warn("'-' is found after '--code'. This is invalid.");
repository = null;
} else {
LOG.info("'-' was found for settings repository, the default settings repository '{}' will be used.", IdeContext.DEFAULT_SETTINGS_REPO_URL);
repository = IdeContext.DEFAULT_SETTINGS_REPO_URL;
}
LOG.info("'-' was found for the repository, the default settings repository '{}' will be used.", IdeContext.DEFAULT_SETTINGS_REPO_URL);
repository = IdeContext.DEFAULT_SETTINGS_REPO_URL;
}
return repository;
}

private void checkProjectNameConvention(String projectName) {
boolean isSettingsRepo = projectName.contains(IdeContext.SETTINGS_REPOSITORY_KEYWORD);
boolean codeRepository = isCodeRepository();
if (isSettingsRepo == codeRepository) {
String warningTemplate;
if (codeRepository) {
warningTemplate = """
Your git URL is pointing to the project name {} that contains the keyword '{}'.
Therefore we assume that you did a mistake by adding the '--code' option to the ide project creation.
Do you really want to create the project?""";
} else {
warningTemplate = """
Your git URL is pointing to the project name {} that does not contain the keyword ''{}''.
Therefore we assume that you forgot to add the '--code' option to the ide project creation.
Do you really want to create the project?""";
}
this.context.askToContinue(warningTemplate, projectName, IdeContext.SETTINGS_REPOSITORY_KEYWORD);
}
}

private void initializeRepository(GitUrl gitUrl) {

GitContext gitContext = this.context.getGitContext();
Path settingsPath = this.context.getSettingsPath();
Path repoPath = settingsPath;
boolean codeRepository = isCodeRepository();
if (codeRepository) {
// clone the given code repository into IDE_HOME/workspaces/main
repoPath = context.getWorkspacePath().resolve(gitUrl.getProjectName());
}
gitContext.pullOrClone(gitUrl, repoPath);
if (codeRepository) {
// check for settings folder and create symlink to IDE_HOME/settings
Path settingsFolder = repoPath.resolve(IdeContext.FOLDER_SETTINGS);
if (Files.exists(settingsFolder)) {
context.getFileAccess().symlink(settingsFolder, settingsPath);
} else {
throw new CliException("Invalid code repository " + gitUrl + ": missing a settings folder at " + settingsFolder);
}
}
this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath());
}


private void updateSoftware() {

if (this.skipTools.isTrue()) {
Expand Down Expand Up @@ -443,14 +399,4 @@ private void createStartScript(String ide, String workspace) {
fileAccess.writeFileContent(scriptContent, scriptPath);
fileAccess.makeExecutable(scriptPath);
}

/**
* Judge if the repository is a code repository.
*
* @return true when the repository is a code repository, otherwise false.
*/
protected boolean isCodeRepository() {
return false;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.devonfw.tools.ide.cli.CliException;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.environment.EnvironmentVariables;
import com.devonfw.tools.ide.io.FileAccess;
import com.devonfw.tools.ide.io.FileCopyMode;
import com.devonfw.tools.ide.log.IdeLogLevel;
import com.devonfw.tools.ide.property.FlagProperty;
import com.devonfw.tools.ide.property.StringProperty;
Expand All @@ -24,9 +27,6 @@ public class CreateCommandlet extends AbstractUpdateCommandlet {
/** {@link StringProperty} for the name of the new project */
public final StringProperty newProject;

/** {@link FlagProperty} for creating a project with settings inside a code repository */
public final FlagProperty codeRepositoryFlag;

/**
* The constructor.
*
Expand All @@ -36,7 +36,6 @@ public CreateCommandlet(IdeContext context) {

super(context);
this.newProject = add(new StringProperty("", true, "project"));
this.codeRepositoryFlag = add(new FlagProperty("--code"));
add(this.settingsRepo);
}

Expand All @@ -57,16 +56,15 @@ protected void doRun() {

String newProjectName = this.newProject.getValue();
Path newProjectPath = this.context.getIdeRoot().resolve(newProjectName);
Path tempProjectPath = this.context.getIdeRoot().resolve("_ide/tmp/projects").resolve(newProjectName);

LOG.info("Creating new IDEasy project in {}", newProjectPath);
if (!this.context.getFileAccess().isEmptyDir(newProjectPath)) {
this.context.askToContinue("Directory {} already exists. Do you want to continue?", newProjectPath);
} else {
this.context.getFileAccess().mkdirs(newProjectPath);
}

initializeProject(newProjectPath);
this.context.setIdeHome(newProjectPath);
initializeProject(tempProjectPath);
this.context.setIdeHome(tempProjectPath);
this.context.verifyIdeMinVersion(true);
super.doRun();
this.context.verifyIdeMinVersion(true);
Expand All @@ -85,14 +83,91 @@ private void initializeProject(Path newInstancePath) {
}

@Override
protected boolean isCodeRepository() {
return this.codeRepositoryFlag.isTrue();
protected void updateSettings() {
super.updateSettings();
analyzeProject();
}

/**
* This method is invoked when a new porject is created. It analyzes the cloned repository to check if it is a valid IDEasy repository. The repository can either be a settings repository (with ide.properties or devon.properties on the top level)
* or a code repository (with a settings folder on the top level containing such a file). Otherwise, the project creation fails and an error message is logged.
*/
private void analyzeProject() {
// Settings repository: ide.properties on top levels (or devon.properties for legacy users)
// Code repository: settings folder on top level with ide.properties inside (or devon.properties for legacy users)
String projectName = this.context.getProjectName();
Path actualProjectPath;
FileAccess fileAccess = this.context.getFileAccess();
Path settingsPath = this.context.getSettingsPath();

// Check whether the repository is a valid settings repository, code repository, or neither
if (isSettingsRepository(settingsPath)) {
LOG.info("The repository seems to be a settings repository based on the presence of " + EnvironmentVariables.DEFAULT_PROPERTIES + " or " + EnvironmentVariables.LEGACY_PROPERTIES + " on the top level.");
actualProjectPath = this.context.getIdeRoot();
moveProject(this.context.getIdeHome(), actualProjectPath);

} else if (isCodeRepository(settingsPath)) {
LOG.info(EnvironmentVariables.DEFAULT_PROPERTIES + " or " + EnvironmentVariables.LEGACY_PROPERTIES + " found in settings subfolder. This indicates a code repository with a settings folder on the top level.");
// Move settings folder contents containing code into workspace/main/<project_name>
actualProjectPath = this.context.getIdeRoot().resolve(projectName).resolve("workspaces/main/").resolve(projectName);
for (Path child : fileAccess.listChildren(settingsPath, f -> true)) {
moveProject(child, actualProjectPath);
}
// Move remaining folders into IDE_ROOT/<project_name>
actualProjectPath = this.context.getIdeRoot();
moveProject(this.context.getIdeHome(), actualProjectPath);
// Delete empty settings folder in IDE_ROOT/<project_name> so we can create a symlink in the next step
fileAccess.delete(actualProjectPath.resolve(projectName).resolve("settings"));
// Link settings folder in IDE_HOME to settings folder in code repository
fileAccess.symlink(actualProjectPath.resolve(projectName).resolve("workspaces/main").resolve(projectName).resolve("settings"), actualProjectPath.resolve(projectName).resolve("settings"));
// Final cleanup in temp location
fileAccess.delete(this.context.getIdeHome());

} else {
// Repository seems to be invalid. Clean up temporary location and return error
fileAccess.delete(this.context.getIdeHome());
throw new CliException("This repository does not include an " + EnvironmentVariables.DEFAULT_PROPERTIES + " or " + EnvironmentVariables.LEGACY_PROPERTIES + " file at the top level or a settings folder with such a file. "
+ "The repository does not seem to be a valid IDEasy repository. Please verify the repository and try again.");
}
// Set IDE_HOME to new (and actual) project location
this.context.setIdeHome(this.context.getIdeRoot().resolve(projectName));
}

/**
* Moves files of a new projectfrom the temporary location to the final project location.
* @param oldPath - The path of the file or directory to be moved.
* @param newPath - The path of the destination.
*/
private void moveProject(Path oldPath, Path newPath) {
FileAccess fileAccess = this.context.getFileAccess();
try {
fileAccess.copy(oldPath, newPath, FileCopyMode.COPY_TREE_OVERRIDE_FILES);
fileAccess.delete(oldPath);
} catch (Exception e) {
LOG.error("Failed to move project from {} to {}. Please move it manually.", oldPath, newPath, e);
}
}

/**
* Checks whether te given repository is a settings repository by checking for the presence of ide.properties or devon.properties on the top level.
* @param repositoryPath - The path of the repository to be checked.
*/
private boolean isSettingsRepository(Path repositoryPath) {
return Files.exists(repositoryPath.resolve(EnvironmentVariables.DEFAULT_PROPERTIES)) || Files.exists(repositoryPath.resolve(EnvironmentVariables.LEGACY_PROPERTIES));
}

/**
* Checks whether te given repository is a code repository by checking for the presence of ide.properties or devon.properties within a settings folder on the top level.
* @param repositoryPath - The path of the repository to be checked.
*/
private boolean isCodeRepository(Path repositoryPath) {
return isSettingsRepository(repositoryPath.resolve(IdeContext.FOLDER_SETTINGS));
}

@Override
protected String getStepMessage() {

return "Create (clone) " + (isCodeRepository() ? "code" : "settings") + " repository";
return "Create (Clone) repository";
}

private void logWelcomeMessage() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.junit.jupiter.params.provider.ValueSource;

import com.devonfw.tools.ide.cli.CliArguments;
import com.devonfw.tools.ide.cli.CliException;
import com.devonfw.tools.ide.context.AbstractIdeContextTest;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.context.IdeTestContext;
Expand Down Expand Up @@ -66,54 +67,7 @@ void testCreateCommandletRun() {
assertThat(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)).exists();
assertThat(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)).exists();
assertThat(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)).exists();
}

@ParameterizedTest
@ValueSource(strings = { "https://some-code-repository", "ssh://some-settings-repository" })
void testWarningWhenRepoDoesNotMeetNamingConvention(String invalidRepo, @TempDir Path tempDir) {
// arrange
ProcessContextGitMock gitMock = new ProcessContextGitMock(context, tempDir);
context.setProcessContext(gitMock);
CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class);
cc.newProject.setValueAsString(NEW_PROJECT_NAME, context);
cc.codeRepositoryFlag.setValue(!invalidRepo.contains("code")); // raise conflict
cc.settingsRepo.setValue(invalidRepo);
cc.skipTools.setValue(true);
context.setAnswers("yes");
// act
cc.run();
// assert
assertThat(context).logAtInteraction().hasMessageContaining("Do you really want to create the project?");
Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME);
assertThat(newProjectPath).exists();
assertThat(context.getIdeHome()).isEqualTo(newProjectPath);
assertThat(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)).exists();
assertThat(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)).exists();
assertThat(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)).exists();
}

@Test
void testWarningWhenCodeRepoUsingDefaultMark(@TempDir Path tempDir) {
String invalidCodeRepo = "-";
// arrange
ProcessContextGitMock gitMock = new ProcessContextGitMock(context, tempDir);
context.setProcessContext(gitMock);
CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class);
cc.newProject.setValueAsString(NEW_PROJECT_NAME, context);
cc.settingsRepo.setValue(invalidCodeRepo);
cc.codeRepositoryFlag.setValue(true);
cc.skipTools.setValue(true);
context.setAnswers("https://some-code-repository");
// act
cc.run();
// assert
assertThat(context).logAtWarning().hasMessageContaining("'-' is found after '--code'. This is invalid.");
Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME);
assertThat(newProjectPath).exists();
assertThat(context.getIdeHome()).isEqualTo(newProjectPath);
assertThat(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)).exists();
assertThat(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)).exists();
assertThat(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)).exists();
assertThat(context.getIdeRoot().resolve("_ide/tmp/projects").resolve(NEW_PROJECT_NAME)).doesNotExist();
}

@Test
Expand Down Expand Up @@ -214,6 +168,35 @@ void testWelcomeMessageDisplayed() {
// assert
Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME);
assertThat(newProjectPath).exists();
assertThat(context.getIdeHome()).isEqualTo(newProjectPath);
assertThat(newProjectPath.resolve(IdeContext.FOLDER_PLUGINS)).exists();
assertThat(newProjectPath.resolve(IdeContext.FOLDER_SOFTWARE)).exists();
assertThat(newProjectPath.resolve(IdeContext.FOLDER_WORKSPACES).resolve(IdeContext.WORKSPACE_MAIN)).exists();
assertThat(context.getIdeRoot().resolve("_ide/tmp/projects").resolve(NEW_PROJECT_NAME)).doesNotExist();
assertThat(context).logAtInfo().hasMessageContaining("Welcome to your new IDEasy project!");
}

@Test
void testProjectWithInvalidRepositoryNotCreated() {

// arrange - create a new project that is invalid (does not contain ide.properties file)
GitContextImplMock gitContextImplMock = new GitContextImplMock(context, TEST_RESOURCES.resolve("pypi"));

context.setGitContext(gitContextImplMock);
CreateCommandlet cc = context.getCommandletManager().getCommandlet(CreateCommandlet.class);
cc.newProject.setValueAsString(NEW_PROJECT_NAME, context);
cc.settingsRepo.setValue(IdeContext.DEFAULT_SETTINGS_REPO_URL);
cc.skipTools.setValue(true);

// act - run the create command
assertThatThrownBy(() -> cc.run())
.isInstanceOf(CliException.class)
.hasMessageContaining("This repository does not include an " + EnvironmentVariables.DEFAULT_PROPERTIES + " or " + EnvironmentVariables.LEGACY_PROPERTIES + " file at the top level or a settings folder with such a file.")
.hasMessageContaining("The repository does not seem to be a valid IDEasy repository. Please verify the repository and try again.");

// assert
Path newProjectPath = context.getIdeRoot().resolve(NEW_PROJECT_NAME);
assertThat(newProjectPath).doesNotExist();
assertThat(context.getIdeRoot().resolve("_ide/tmp/projects").resolve(NEW_PROJECT_NAME)).doesNotExist();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public IdeTestContext(Path workingDirectory, IdeLogLevel logLevel, WireMockRunti
private IdeTestContext(IdeTestStartContext startContext, Path workingDirectory, WireMockRuntimeInfo wireMockRuntimeInfo) {

super(startContext, workingDirectory, wireMockRuntimeInfo);
this.gitContext = new GitContextMock();
this.gitContext = new GitContextMock(this);
}

@Override
Expand Down
Loading
Loading