diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 922c15a95..1b8260226 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,8 @@ 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/757[#757]: Support to allow settings in code repository +* https://github.com/devonfw/IDEasy/issues/826[#826]: Fix git settings check when settings folder is empty * https://github.com/devonfw/IDEasy/issues/894[#894]: Fix ide.bat printing for initialization and error output * https://github.com/devonfw/IDEasy/issues/759[#759]: Add UpgradeSettingsCommandlet for the upgrade of legacy devonfw-ide settings to IDEasy diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java index 54c985f03..60b7b0ab6 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/AbstractUpdateCommandlet.java @@ -106,7 +106,11 @@ private void setupConf(Path template, Path conf) { } } - private void updateSettings() { + /** + * Updates the settings repository in IDE_HOME/settings by either cloning if no such repository exists or pulling + * if the repository exists then saves the latest current commit ID in the file ".commit.id". + */ + protected void updateSettings() { Path settingsPath = this.context.getSettingsPath(); GitContext gitContext = this.context.getGitContext(); @@ -132,6 +136,7 @@ private void updateSettings() { } gitContext.pullOrClone(GitUrl.of(repository), settingsPath); } + this.context.getGitContext().saveCurrentCommitId(settingsPath, this.context.getSettingsCommitIdPath()); step.success("Successfully updated settings repository."); } finally { if (step != null) { diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java index 4dc07641b..f065d614f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CreateCommandlet.java @@ -1,8 +1,10 @@ package com.devonfw.tools.ide.commandlet; +import java.nio.file.Files; import java.nio.file.Path; import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.git.GitUrl; import com.devonfw.tools.ide.io.FileAccess; import com.devonfw.tools.ide.property.FlagProperty; import com.devonfw.tools.ide.property.StringProperty; @@ -18,6 +20,9 @@ public class CreateCommandlet extends AbstractUpdateCommandlet { /** {@link FlagProperty} for skipping the setup of git repositories */ public final FlagProperty skipRepositories; + /** {@link FlagProperty} for creating a project with settings inside a code repository */ + public final FlagProperty codeRepositoryFlag; + /** * The constructor. * @@ -28,6 +33,7 @@ public CreateCommandlet(IdeContext context) { super(context); this.newProject = add(new StringProperty("", true, "project")); this.skipRepositories = add(new FlagProperty("--skip-repositories")); + this.codeRepositoryFlag = add(new FlagProperty("--code")); add(this.settingsRepo); } @@ -59,12 +65,32 @@ public void run() { initializeProject(newProjectPath); this.context.setIdeHome(newProjectPath); super.run(); + if (this.skipRepositories.isTrue()) { this.context.info("Skipping the cloning of project repositories as specified by the user."); } else { updateRepositories(); } this.context.success("Successfully created new project '{}'.", newProjectName); + + } + + private void initializeCodeRepository(String repoUrl) { + + // clone the given repository into IDE_HOME/workspaces/main + GitUrl gitUrl = GitUrl.of(repoUrl); + Path codeRepoPath = this.context.getWorkspacePath().resolve(gitUrl.getProjectName()); + this.context.getGitContext().pullOrClone(gitUrl, codeRepoPath); + + // check for settings folder and create symlink to IDE_HOME/settings + Path settingsFolder = codeRepoPath.resolve(IdeContext.FOLDER_SETTINGS); + if (Files.exists(settingsFolder)) { + this.context.getFileAccess().symlink(settingsFolder, this.context.getSettingsPath()); + // create a file in IDE_HOME with the current local commit id + this.context.getGitContext().saveCurrentCommitId(codeRepoPath, this.context.getSettingsCommitIdPath()); + } else { + this.context.warning("No settings folder was found inside the code repository."); + } } private void initializeProject(Path newInstancePath) { @@ -79,4 +105,25 @@ private void updateRepositories() { this.context.getCommandletManager().getCommandlet(RepositoryCommandlet.class).run(); } + + @Override + protected void updateSettings() { + + if (codeRepositoryFlag.isTrue()) { + String codeRepository = this.settingsRepo.getValue(); + if (codeRepository == null || codeRepository.isBlank()) { + String message = """ + No code repository was given after '--code'. + Please give the code repository below that includes your settings folder. + Further details can be found here: https://github.com/devonfw/IDEasy/blob/main/documentation/settings.adoc + Code repository URL: + """; + codeRepository = this.context.askForInput(message); + } + initializeCodeRepository(codeRepository); + } else { + super.updateSettings(); + } + + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java index b706d944a..94d4401e4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/StatusCommandlet.java @@ -55,10 +55,10 @@ private void logSettingsLegacyStatus() { } private void logSettingsGitStatus() { - Path settingsPath = this.context.getSettingsPath(); + Path settingsPath = this.context.getSettingsGitRepository(); if (settingsPath != null) { GitContext gitContext = this.context.getGitContext(); - if (gitContext.isRepositoryUpdateAvailable(settingsPath)) { + if (gitContext.isRepositoryUpdateAvailable(settingsPath, this.context.getSettingsCommitIdPath())) { this.context.warning("Your settings are not up-to-date, please run 'ide update'."); } else { this.context.success("Your settings are up-to-date."); diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java index d1fb11cc2..f571c4f3d 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/AbstractIdeContext.java @@ -77,6 +77,8 @@ public abstract class AbstractIdeContext implements IdeContext { protected Path settingsPath; + private Path settingsCommitIdPath; + private Path softwarePath; private Path softwareExtraPath; @@ -241,6 +243,7 @@ public void setCwd(Path userDir, String workspace, Path ideHome) { this.workspacePath = this.ideHome.resolve(FOLDER_WORKSPACES).resolve(this.workspaceName); this.confPath = this.ideHome.resolve(FOLDER_CONF); this.settingsPath = this.ideHome.resolve(FOLDER_SETTINGS); + this.settingsCommitIdPath = this.ideHome.resolve(IdeContext.SETTINGS_COMMIT_ID); this.softwarePath = this.ideHome.resolve(FOLDER_SOFTWARE); this.softwareExtraPath = this.softwarePath.resolve(FOLDER_EXTRA); this.pluginsPath = this.ideHome.resolve(FOLDER_PLUGINS); @@ -409,6 +412,31 @@ public Path getSettingsPath() { return this.settingsPath; } + @Override + public Path getSettingsGitRepository() { + + Path settingsPath = getSettingsPath(); + + if (settingsPath == null) { + error("No settings repository was found."); + return null; + } + + // check whether the settings path has a .git folder only if its not a symbolic link + if (!Files.exists(settingsPath.resolve(".git")) && !Files.isSymbolicLink(settingsPath)) { + error("Settings repository exists but is not a git repository."); + return null; + } + + return settingsPath; + } + + @Override + public Path getSettingsCommitIdPath() { + + return this.settingsCommitIdPath; + } + @Override public Path getConfPath() { @@ -836,10 +864,11 @@ private ValidationResult applyAndRun(CliArguments arguments, Commandlet cmd) { if (cmd.isIdeHomeRequired()) { debug(getMessageIdeHomeFound()); } - if (this.settingsPath != null) { - if (getGitContext().isRepositoryUpdateAvailable(this.settingsPath) || - (getGitContext().fetchIfNeeded(this.settingsPath) && getGitContext().isRepositoryUpdateAvailable(this.settingsPath))) { - interaction("Updates are available for the settings repository. If you want to pull the latest changes, call ide update."); + Path settingsRepository = getSettingsGitRepository(); + if (settingsRepository != null) { + if (getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()) || + (getGitContext().fetchIfNeeded(settingsRepository) && getGitContext().isRepositoryUpdateAvailable(settingsRepository, getSettingsCommitIdPath()))) { + interaction("Updates are available for the settings repository. If you want to apply the latest changes, call \"ide update\""); } } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java index 9b962655a..c1ee5dd3f 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java @@ -139,6 +139,10 @@ public interface IdeContext extends IdeStartContext { /** The filename of the configuration file in the settings for this {@link CustomToolRepository}. */ String FILE_CUSTOM_TOOLS = "ide-custom-tools.json"; + /** + * file containing the current local commit hash of the settings repository. */ + String SETTINGS_COMMIT_ID = ".commit.id"; + /** * @return {@code true} if {@link #isOfflineMode() offline mode} is active or we are NOT {@link #isOnline() online}, {@code false} otherwise. */ @@ -354,6 +358,17 @@ default void requireOnline(String purpose) { */ Path getSettingsPath(); + /** + * + * @return the {@link Path} to the {@code settings} folder with the cloned git repository containing the project configuration only if the settings repository is in fact a git repository. + */ + Path getSettingsGitRepository(); + + /** + * @return the {@link Path} to the file containing the last tracked commit Id of the settings repository. + */ + Path getSettingsCommitIdPath(); + /** * @return the {@link Path} to the templates folder inside the {@link #getSettingsPath() settings}. The relative directory structure in this templates folder * is to be applied to {@link #getIdeHome() IDE_HOME} when the project is set up. diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java index fe5af87e9..6a27c4de4 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java @@ -58,13 +58,23 @@ public interface GitContext { * Checks if there are updates available for the Git repository in the specified target folder by comparing the local commit hash with the remote commit * hash. * - * @param repository the {@link Path} to the target folder where the git repository is located. This should be the folder containing the ".git" - * subfolder. + * @param repository the {@link Path} to the target folder where the git repository is located. * @return {@code true} if the remote repository contains commits that are not present in the local repository, indicating that updates are available. * {@code false} if the local and remote repositories are in sync, or if there was an issue retrieving the commit hashes. */ boolean isRepositoryUpdateAvailable(Path repository); + /** + * Checks if there are updates available for the Git repository in the specified target folder by comparing the local commit hash with the remote commit + * hash. + * + * @param repository the {@link Path} to the target folder where the git repository is located. + * @param trackedCommitIdPath the {@link Path} to a file containing the last tracked commit ID of this repository. + * @return {@code true} if the remote repository contains commits that are not present in the local repository, indicating that updates are available. + * {@code false} if the local and remote repositories are in sync, or if there was an issue retrieving the commit hashes. + */ + boolean isRepositoryUpdateAvailable(Path repository, Path trackedCommitIdPath); + /** * Attempts a git pull and reset if required. * @@ -175,4 +185,11 @@ default void reset(Path repository, String branch) { */ String determineRemote(Path repository); + /** + * Saves the current git commit ID of a repository to a file given as an argument. + * + * @param repository the path to the git repository + * @param trackedCommitIdPath the path to the file where the commit Id will be written. + */ + void saveCurrentCommitId(Path repository, Path trackedCommitIdPath); } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java index eda59a8e4..185f9e2a3 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitContextImpl.java @@ -1,5 +1,6 @@ package com.devonfw.tools.ide.git; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -59,6 +60,21 @@ public boolean isRepositoryUpdateAvailable(Path repository) { return !localCommitId.equals(remoteCommitId); } + @Override + public boolean isRepositoryUpdateAvailable(Path repository, Path trackedCommitIdPath) { + + verifyGitInstalled(); + String trackedCommitId; + try { + trackedCommitId = Files.readString(trackedCommitIdPath); + } catch (IOException e) { + return false; + } + + String remoteCommitId = runGitCommandAndGetSingleOutput("Failed to get the remote commit id.", repository, "rev-parse", "@{u}"); + return !trackedCommitId.equals(remoteCommitId); + } + @Override public void pullOrCloneAndResetIfNeeded(GitUrl gitUrl, Path repository, String remoteName) { @@ -292,6 +308,24 @@ private ProcessResult runGitCommand(Path directory, ProcessMode mode, ProcessErr processContext.addArgs(args); return processContext.run(mode); } + + @Override + public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) { + + if ((repository == null) || (trackedCommitIdPath == null)) { + this.context.warning("Invalid usage of saveCurrentCommitId with null value"); + return; + } + this.context.trace("Saving commit Id of {} into {}", repository, trackedCommitIdPath); + String currentCommitId = runGitCommandAndGetSingleOutput("Failed to get current commit id.", repository, "rev-parse", "HEAD"); + if (currentCommitId != null) { + try { + Files.writeString(trackedCommitIdPath, currentCommitId); + } catch (IOException e) { + throw new IllegalStateException("Failed to save commit ID", e); + } + } + } } diff --git a/cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java index 2245f155d..c77d7f281 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/git/GitUrl.java @@ -43,6 +43,21 @@ public String toString() { return this.url + "#" + this.branch; } + /** + * Extracts the project name from an git URL. + * For URLs like "https://github.com/devonfw/ide-urls.git" returns "ide-urls" + * + * @return the project name without ".git" extension + */ + public String getProjectName() { + String path = this.url.substring(this.url.indexOf("://") + 3); + if (path.endsWith(".git")) { + path = path.substring(0, path.length() - 4); + } + String[] parts = path.split("/"); + return parts[parts.length - 1]; + } + /** * @param gitUrl the {@link #toString() string representation} of a {@link GitUrl}. May contain a branch name as {@code «url»#«branch»}. * @return the parsed {@link GitUrl}. diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index 043f683e5..9ee1535bc 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -116,6 +116,7 @@ cmd.vscode=Tool commandlet for Visual Studio Code (IDE). cmd.vscode.detail=Visual Studio Code (VS Code) is a popular code editor developed by Microsoft. Detailed documentation can be found at https://code.visualstudio.com/docs/ commandlets=Available commandlets: opt.--batch=enable batch mode (non-interactive). +opt.--code=clone given code repository containing a settings folder into workspaces so that settings can be committed alongside code changes. opt.--debug=enable debug logging. opt.--force=enable force mode. opt.--locale=the locale (e.g. '--locale=de' for German language). diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index 35ad45187..a73ea56e3 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -116,6 +116,7 @@ cmd.vscode=Werkzeug Kommando für Visual Studio Code (IDE). cmd.vscode.detail=Visual Studio Code (VS Code) ist ein beliebter Code-Editor, der von Microsoft entwickelt wurde. Detaillierte Dokumentation ist zu finden unter https://code.visualstudio.com/docs/ commandlets=Verfügbare Kommandos: opt.--batch=Aktiviert den Batch-Modus (nicht-interaktive Stapelverarbeitung). +opt.--code=Git-Repository sowohl als Code- als auch als Settings-Repository verwenden. opt.--debug=Aktiviert Debug-Ausgaben (Fehleranalyse). opt.--force=Aktiviert den Force-Modus (Erzwingen). opt.--locale=Die Spracheinstellungen (z.B. 'en' für Englisch). diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/HelpCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/HelpCommandletTest.java index 02b1d599f..ad480c094 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/commandlet/HelpCommandletTest.java +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/HelpCommandletTest.java @@ -29,7 +29,7 @@ public class HelpCommandletTest extends AbstractIdeContextTest { * Test of {@link HelpCommandlet} does not require home. */ @Test - public void testThatHomeIsNotReqired() { + public void testThatHomeIsNotRequired() { // arrange IdeContext context = IdeTestContextMock.get(); diff --git a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java index 014655d9b..3afb4ba52 100644 --- a/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java +++ b/cli/src/test/java/com/devonfw/tools/ide/git/GitContextMock.java @@ -73,6 +73,12 @@ public boolean isRepositoryUpdateAvailable(Path repository) { return false; } + @Override + public boolean isRepositoryUpdateAvailable(Path repository, Path trackedCommitIdPath) { + + return false; + } + @Override public String determineCurrentBranch(Path repository) { @@ -84,4 +90,10 @@ public String determineRemote(Path repository) { return "origin"; } + + + @Override + public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) { + + } } diff --git a/documentation/settings.adoc b/documentation/settings.adoc index 721404568..9b5e59bb5 100644 --- a/documentation/settings.adoc +++ b/documentation/settings.adoc @@ -9,9 +9,18 @@ To get an initial set of these settings we provide the default https://github.co These are also released so you can download the latest stable or any history version at http://search.maven.org/#search|ga|1|a%3A%22devonfw-ide-settings%22[maven central]. To test `IDEasy` or for very small projects you can also use these the latest default settings (just hit return when link:setup.adoc[setup] is asking for the `Settings URL`). -However, for collaborative projects we strongly encourage you to distribute and maintain the settings via a dedicated and project specific `git` repository. +However, for collaborative projects we provide two approaches to distribute and maintain the settings: + +* Via a dedicated and project specific git repository (recommended approach). This gives you the freedom to control and manage the tools with their versions and configurations during the project lifecycle. Therefore simply follow the link:usage.adoc#admin[admin usage guide]. +* Via your code repository by including the settings folder directly in your project. +This allows you to keep settings changes in sync with code changes and manage them in the same pull requests. +To use this approach: +** Create a settings folder in your repository root following the structure described below +** Use `ide create --code ` to create your project. +IDEasy will clone your repository and create a symlink to the settings folder. +Changes to settings can then be committed alongside code changes. == Structure