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 78df57524..176da2df7 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,7 @@ private void setupConf(Path template, Path conf) { } } - private void updateSettings() { + protected void updateSettings() { Path settingsPath = this.context.getSettingsPath(); GitContext gitContext = this.context.getGitContext(); @@ -132,6 +132,7 @@ private void updateSettings() { } gitContext.pullOrClone(GitUrl.of(repository), settingsPath); } + this.context.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..a23e8e70b 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.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,15 @@ private void updateRepositories() { this.context.getCommandletManager().getCommandlet(RepositoryCommandlet.class).run(); } + + @Override + protected void updateSettings() { + + if (codeRepositoryFlag.isTrue() && !settingsRepo.getValue().isBlank()) { + String repoUrl = settingsRepo.getValue(); + initializeCodeRepository(repoUrl); + } 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 eb5e16e46..9c1fa1128 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 @@ -1,6 +1,7 @@ package com.devonfw.tools.ide.context; import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.net.URLConnection; @@ -12,6 +13,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import com.devonfw.tools.ide.cli.CliAbortException; import com.devonfw.tools.ide.cli.CliArgument; @@ -77,6 +79,8 @@ public abstract class AbstractIdeContext implements IdeContext { protected Path settingsPath; + private Path settingsCommitIdPath; + private Path softwarePath; private Path softwareExtraPath; @@ -241,6 +245,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); @@ -430,6 +435,30 @@ public Path getSettingsPath() { return this.settingsPath; } + @Override + public Path getSettingsGitRepository() { + + Path settingsPath = getSettingsPath(); + + if (Objects.isNull(settingsPath)) { + error("No settings repository was found."); + } + + // 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() { @@ -857,10 +886,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\""); } } } @@ -1123,4 +1153,21 @@ public IdeStartContextImpl getStartContext() { public void reload() { this.variables = null; } + + @Override + public void saveCurrentCommitId(Path repository, Path trackedCommitIdPath) { + + trace("Saving commit Id of {} into {}", repository, trackedCommitIdPath); + if (Objects.isNull(repository)) { + return; + } + String currentCommitId = getGitContext().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/context/IdeContext.java b/cli/src/main/java/com/devonfw/tools/ide/context/IdeContext.java index 0273629a5..3935caf67 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 @@ -136,6 +136,10 @@ public interface IdeContext extends IdeStartContext { /** Legacy folder name used as compatibility fallback if {@link #FOLDER_TEMPLATES} does not exist. */ String FOLDER_LEGACY_TEMPLATES = "devon"; + /** + * 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. */ @@ -351,6 +355,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. @@ -588,4 +603,12 @@ default String findBashRequired() { */ void logIdeHomeAndRootStatus(); + /** + * 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/GitContext.java b/cli/src/main/java/com/devonfw/tools/ide/git/GitContext.java index fe5af87e9..1f740ec97 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,15 @@ default void reset(Path repository, String branch) { */ String determineRemote(Path repository); + /** + * Executes a Git command and returns a single line of output. + * + * @param warningOnError The warning message to log if the command fails or produces unexpected output + * @param directory The directory in which to execute the Git command + * @param args Variable number of command arguments to pass to Git + * @return The single line of output if successful and exactly one line is produced, null otherwise + * (with a warning logged containing warningOnError and additional context) + */ + String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, String... args); + } 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..401921eda 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) { @@ -262,7 +278,7 @@ private void runGitCommand(Path directory, List args) { runGitCommand(directory, args.toArray(String[]::new)); } - private String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, String... args) { + public String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, String... args) { ProcessResult result = runGitCommand(directory, ProcessMode.DEFAULT_CAPTURE, args); if (result.isSuccessful()) { 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 42486803f..98f8508a7 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -114,6 +114,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=use git repository as both code and settings repository 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 3e7af8e8c..150946864 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -114,6 +114,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..7993f1435 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 String runGitCommandAndGetSingleOutput(String warningOnError, Path directory, String... args) { + + return null; + } } 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